Automatic sources dropoff on 2020-06-10 18:32:38.095721
The change is generated with prebuilt drop tool.
Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/com/android/internal/widget/AbsActionBarView.java b/com/android/internal/widget/AbsActionBarView.java
new file mode 100644
index 0000000..0f0c1a3
--- /dev/null
+++ b/com/android/internal/widget/AbsActionBarView.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ActionMenuPresenter;
+import android.widget.ActionMenuView;
+
+import com.android.internal.R;
+
+public abstract class AbsActionBarView extends ViewGroup {
+ private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator();
+
+ private static final int FADE_DURATION = 200;
+
+ protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener();
+
+ /** Context against which to inflate popup menus. */
+ protected final Context mPopupContext;
+
+ protected ActionMenuView mMenuView;
+ protected ActionMenuPresenter mActionMenuPresenter;
+ protected ViewGroup mSplitView;
+ protected boolean mSplitActionBar;
+ protected boolean mSplitWhenNarrow;
+ protected int mContentHeight;
+
+ protected Animator mVisibilityAnim;
+
+ private boolean mEatingTouch;
+ private boolean mEatingHover;
+
+ public AbsActionBarView(Context context) {
+ this(context, null);
+ }
+
+ public AbsActionBarView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AbsActionBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AbsActionBarView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedValue tv = new TypedValue();
+ if (context.getTheme().resolveAttribute(R.attr.actionBarPopupTheme, tv, true)
+ && tv.resourceId != 0) {
+ mPopupContext = new ContextThemeWrapper(context, tv.resourceId);
+ } else {
+ mPopupContext = context;
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ // Action bar can change size on configuration changes.
+ // Reread the desired height from the theme-specified style.
+ TypedArray a = getContext().obtainStyledAttributes(null, R.styleable.ActionBar,
+ com.android.internal.R.attr.actionBarStyle, 0);
+ setContentHeight(a.getLayoutDimension(R.styleable.ActionBar_height, 0));
+ a.recycle();
+ if (mSplitWhenNarrow) {
+ setSplitToolbar(getContext().getResources().getBoolean(
+ com.android.internal.R.bool.split_action_bar_is_narrow));
+ }
+ if (mActionMenuPresenter != null) {
+ mActionMenuPresenter.onConfigurationChanged(newConfig);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ // ActionBarViews always eat touch events, but should still respect the touch event dispatch
+ // contract. If the normal View implementation doesn't want the events, we'll just silently
+ // eat the rest of the gesture without reporting the events to the default implementation
+ // since that's what it expects.
+
+ final int action = ev.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mEatingTouch = false;
+ }
+
+ if (!mEatingTouch) {
+ final boolean handled = super.onTouchEvent(ev);
+ if (action == MotionEvent.ACTION_DOWN && !handled) {
+ mEatingTouch = true;
+ }
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mEatingTouch = false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent ev) {
+ // Same deal as onTouchEvent() above. Eat all hover events, but still
+ // respect the touch event dispatch contract.
+
+ final int action = ev.getActionMasked();
+ if (action == MotionEvent.ACTION_HOVER_ENTER) {
+ mEatingHover = false;
+ }
+
+ if (!mEatingHover) {
+ final boolean handled = super.onHoverEvent(ev);
+ if (action == MotionEvent.ACTION_HOVER_ENTER && !handled) {
+ mEatingHover = true;
+ }
+ }
+
+ if (action == MotionEvent.ACTION_HOVER_EXIT
+ || action == MotionEvent.ACTION_CANCEL) {
+ mEatingHover = false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets whether the bar should be split right now, no questions asked.
+ * @param split true if the bar should split
+ */
+ public void setSplitToolbar(boolean split) {
+ mSplitActionBar = split;
+ }
+
+ /**
+ * Sets whether the bar should split if we enter a narrow screen configuration.
+ * @param splitWhenNarrow true if the bar should check to split after a config change
+ */
+ public void setSplitWhenNarrow(boolean splitWhenNarrow) {
+ mSplitWhenNarrow = splitWhenNarrow;
+ }
+
+ public void setContentHeight(int height) {
+ mContentHeight = height;
+ requestLayout();
+ }
+
+ public int getContentHeight() {
+ return mContentHeight;
+ }
+
+ public void setSplitView(ViewGroup splitView) {
+ mSplitView = splitView;
+ }
+
+ /**
+ * @return Current visibility or if animating, the visibility being animated to.
+ */
+ public int getAnimatedVisibility() {
+ if (mVisibilityAnim != null) {
+ return mVisAnimListener.mFinalVisibility;
+ }
+ return getVisibility();
+ }
+
+ public Animator setupAnimatorToVisibility(int visibility, long duration) {
+ if (mVisibilityAnim != null) {
+ mVisibilityAnim.cancel();
+ }
+
+ if (visibility == VISIBLE) {
+ if (getVisibility() != VISIBLE) {
+ setAlpha(0);
+ if (mSplitView != null && mMenuView != null) {
+ mMenuView.setAlpha(0);
+ }
+ }
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, View.ALPHA, 1);
+ anim.setDuration(duration);
+ anim.setInterpolator(sAlphaInterpolator);
+ if (mSplitView != null && mMenuView != null) {
+ AnimatorSet set = new AnimatorSet();
+ ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, View.ALPHA, 1);
+ splitAnim.setDuration(duration);
+ set.addListener(mVisAnimListener.withFinalVisibility(visibility));
+ set.play(anim).with(splitAnim);
+ return set;
+ } else {
+ anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+ return anim;
+ }
+ } else {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, View.ALPHA, 0);
+ anim.setDuration(duration);
+ anim.setInterpolator(sAlphaInterpolator);
+ if (mSplitView != null && mMenuView != null) {
+ AnimatorSet set = new AnimatorSet();
+ ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, View.ALPHA, 0);
+ splitAnim.setDuration(duration);
+ set.addListener(mVisAnimListener.withFinalVisibility(visibility));
+ set.play(anim).with(splitAnim);
+ return set;
+ } else {
+ anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+ return anim;
+ }
+ }
+ }
+
+ public void animateToVisibility(int visibility) {
+ Animator anim = setupAnimatorToVisibility(visibility, FADE_DURATION);
+ anim.start();
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ if (visibility != getVisibility()) {
+ if (mVisibilityAnim != null) {
+ mVisibilityAnim.end();
+ }
+ super.setVisibility(visibility);
+ }
+ }
+
+ public boolean showOverflowMenu() {
+ if (mActionMenuPresenter != null) {
+ return mActionMenuPresenter.showOverflowMenu();
+ }
+ return false;
+ }
+
+ public void postShowOverflowMenu() {
+ post(new Runnable() {
+ public void run() {
+ showOverflowMenu();
+ }
+ });
+ }
+
+ public boolean hideOverflowMenu() {
+ if (mActionMenuPresenter != null) {
+ return mActionMenuPresenter.hideOverflowMenu();
+ }
+ return false;
+ }
+
+ public boolean isOverflowMenuShowing() {
+ if (mActionMenuPresenter != null) {
+ return mActionMenuPresenter.isOverflowMenuShowing();
+ }
+ return false;
+ }
+
+ public boolean isOverflowMenuShowPending() {
+ if (mActionMenuPresenter != null) {
+ return mActionMenuPresenter.isOverflowMenuShowPending();
+ }
+ return false;
+ }
+
+ public boolean isOverflowReserved() {
+ return mActionMenuPresenter != null && mActionMenuPresenter.isOverflowReserved();
+ }
+
+ public boolean canShowOverflowMenu() {
+ return isOverflowReserved() && getVisibility() == VISIBLE;
+ }
+
+ @UnsupportedAppUsage
+ public void dismissPopupMenus() {
+ if (mActionMenuPresenter != null) {
+ mActionMenuPresenter.dismissPopupMenus();
+ }
+ }
+
+ protected int measureChildView(View child, int availableWidth, int childSpecHeight,
+ int spacing) {
+ child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ childSpecHeight);
+
+ availableWidth -= child.getMeasuredWidth();
+ availableWidth -= spacing;
+
+ return Math.max(0, availableWidth);
+ }
+
+ static protected int next(int x, int val, boolean isRtl) {
+ return isRtl ? x - val : x + val;
+ }
+
+ protected int positionChild(View child, int x, int y, int contentHeight, boolean reverse) {
+ int childWidth = child.getMeasuredWidth();
+ int childHeight = child.getMeasuredHeight();
+ int childTop = y + (contentHeight - childHeight) / 2;
+
+ if (reverse) {
+ child.layout(x - childWidth, childTop, x, childTop + childHeight);
+ } else {
+ child.layout(x, childTop, x + childWidth, childTop + childHeight);
+ }
+
+ return (reverse ? -childWidth : childWidth);
+ }
+
+ protected class VisibilityAnimListener implements Animator.AnimatorListener {
+ private boolean mCanceled = false;
+ int mFinalVisibility;
+
+ public VisibilityAnimListener withFinalVisibility(int visibility) {
+ mFinalVisibility = visibility;
+ return this;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setVisibility(VISIBLE);
+ mVisibilityAnim = animation;
+ mCanceled = false;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCanceled) return;
+
+ mVisibilityAnim = null;
+ setVisibility(mFinalVisibility);
+ if (mSplitView != null && mMenuView != null) {
+ mMenuView.setVisibility(mFinalVisibility);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCanceled = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ }
+}
diff --git a/com/android/internal/widget/AccountItemView.java b/com/android/internal/widget/AccountItemView.java
new file mode 100644
index 0000000..a521428
--- /dev/null
+++ b/com/android/internal/widget/AccountItemView.java
@@ -0,0 +1,102 @@
+/*
+* Copyright (C) 2011-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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.widget.AccountViewAdapter.AccountElements;
+
+
+/**
+ * An LinearLayout view, to show Accounts elements.
+ */
+public class AccountItemView extends LinearLayout {
+
+ private ImageView mAccountIcon;
+ private TextView mAccountName;
+ private TextView mAccountNumber;
+
+ /**
+ * Constructor.
+ */
+ public AccountItemView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructor.
+ */
+ public AccountItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ LayoutInflater inflator = (LayoutInflater)
+ context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflator.inflate(R.layout.simple_account_item, null);
+ addView(view);
+ initViewItem(view);
+ }
+
+ private void initViewItem(View view) {
+ mAccountIcon = (ImageView)view.findViewById(android.R.id.icon);
+ mAccountName = (TextView)view.findViewById(android.R.id.title);
+ mAccountNumber = (TextView)view.findViewById(android.R.id.summary);
+ }
+
+ public void setViewItem(AccountElements element) {
+ Drawable drawable = element.getDrawable();
+ if (drawable != null) {
+ setAccountIcon(drawable);
+ } else {
+ setAccountIcon(element.getIcon());
+ }
+ setAccountName(element.getName());
+ setAccountNumber(element.getNumber());
+ }
+
+ public void setAccountIcon(int resId) {
+ mAccountIcon.setImageResource(resId);
+ }
+
+ public void setAccountIcon(Drawable drawable) {
+ mAccountIcon.setBackgroundDrawable(drawable);
+ }
+
+ public void setAccountName(String name) {
+ setText(mAccountName, name);
+ }
+
+ public void setAccountNumber(String number) {
+ setText(mAccountNumber, number);
+ }
+
+ private void setText(TextView view, String text) {
+ if (TextUtils.isEmpty(text)) {
+ view.setVisibility(View.GONE);
+ } else {
+ view.setText(text);
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+}
diff --git a/com/android/internal/widget/AccountViewAdapter.java b/com/android/internal/widget/AccountViewAdapter.java
new file mode 100644
index 0000000..8a7a9a6
--- /dev/null
+++ b/com/android/internal/widget/AccountViewAdapter.java
@@ -0,0 +1,127 @@
+/*
+* Copyright (C) 2011-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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import java.util.List;
+
+public class AccountViewAdapter extends BaseAdapter {
+
+ private List<AccountElements> mData;
+ private Context mContext;
+
+ /**
+ * Constructor
+ *
+ * @param context The context where the View associated with this Adapter is running
+ * @param data A list with AccountElements data type. The list contains the data of each
+ * account and the each member of AccountElements will correspond to one item view.
+ */
+ public AccountViewAdapter(Context context, final List<AccountElements> data) {
+ mContext = context;
+ mData = data;
+ }
+
+ @Override
+ public int getCount() {
+ return mData.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mData.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public void updateData(final List<AccountElements> data) {
+ mData = data;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ AccountItemView view;
+ if (convertView == null) {
+ view = new AccountItemView(mContext);
+ } else {
+ view = (AccountItemView) convertView;
+ }
+ AccountElements elements = (AccountElements) getItem(position);
+ view.setViewItem(elements);
+ return view;
+ }
+
+ public static class AccountElements {
+ private int mIcon;
+ private Drawable mDrawable;
+ private String mName;
+ private String mNumber;
+
+ /**
+ * Constructor
+ * A structure with basic element of an Account, icon, name and number
+ *
+ * @param icon Account icon id
+ * @param name Account name
+ * @param num Account number
+ */
+ public AccountElements(int icon, String name, String number) {
+ this(icon, null, name, number);
+ }
+
+ /**
+ * Constructor
+ * A structure with basic element of an Account, drawable, name and number
+ *
+ * @param drawable Account drawable
+ * @param name Account name
+ * @param num Account number
+ */
+ public AccountElements(Drawable drawable, String name, String number) {
+ this(0, drawable, name, number);
+ }
+
+ private AccountElements(int icon, Drawable drawable, String name, String number) {
+ mIcon = icon;
+ mDrawable = drawable;
+ mName = name;
+ mNumber = number;
+ }
+
+ public int getIcon() {
+ return mIcon;
+ }
+ public String getName() {
+ return mName;
+ }
+ public String getNumber() {
+ return mNumber;
+ }
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+ }
+}
diff --git a/com/android/internal/widget/ActionBarAccessor.java b/com/android/internal/widget/ActionBarAccessor.java
new file mode 100644
index 0000000..40b6220
--- /dev/null
+++ b/com/android/internal/widget/ActionBarAccessor.java
@@ -0,0 +1,32 @@
+/*
+ * 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.internal.widget;
+
+import android.widget.ActionMenuPresenter;
+
+/**
+ * To access non public members of AbsActionBarView
+ */
+public class ActionBarAccessor {
+
+ /**
+ * Returns the {@link ActionMenuPresenter} associated with the {@link AbsActionBarView}
+ */
+ public static ActionMenuPresenter getActionMenuPresenter(AbsActionBarView view) {
+ return view.mActionMenuPresenter;
+ }
+}
diff --git a/com/android/internal/widget/ActionBarContainer.java b/com/android/internal/widget/ActionBarContainer.java
new file mode 100644
index 0000000..baf3188
--- /dev/null
+++ b/com/android/internal/widget/ActionBarContainer.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * This class acts as a container for the action bar view and action mode context views.
+ * It applies special styles as needed to help handle animated transitions between them.
+ * @hide
+ */
+public class ActionBarContainer extends FrameLayout {
+ private boolean mIsTransitioning;
+ private View mTabContainer;
+ private View mActionBarView;
+ private View mActionContextView;
+
+ private Drawable mBackground;
+ private Drawable mStackedBackground;
+ private Drawable mSplitBackground;
+ private boolean mIsSplit;
+ private boolean mIsStacked;
+ private int mHeight;
+
+ public ActionBarContainer(Context context) {
+ this(context, null);
+ }
+
+ public ActionBarContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Set a transparent background so that we project appropriately.
+ setBackground(new ActionBarBackgroundDrawable());
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ActionBar);
+ mBackground = a.getDrawable(com.android.internal.R.styleable.ActionBar_background);
+ mStackedBackground = a.getDrawable(
+ com.android.internal.R.styleable.ActionBar_backgroundStacked);
+ mHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.ActionBar_height, -1);
+
+ if (getId() == com.android.internal.R.id.split_action_bar) {
+ mIsSplit = true;
+ mSplitBackground = a.getDrawable(
+ com.android.internal.R.styleable.ActionBar_backgroundSplit);
+ }
+ a.recycle();
+
+ setWillNotDraw(mIsSplit ? mSplitBackground == null :
+ mBackground == null && mStackedBackground == null);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ super.onFinishInflate();
+ mActionBarView = findViewById(com.android.internal.R.id.action_bar);
+ mActionContextView = findViewById(com.android.internal.R.id.action_context_bar);
+ }
+
+ public void setPrimaryBackground(Drawable bg) {
+ if (mBackground != null) {
+ mBackground.setCallback(null);
+ unscheduleDrawable(mBackground);
+ }
+ mBackground = bg;
+ if (bg != null) {
+ bg.setCallback(this);
+ if (mActionBarView != null) {
+ mBackground.setBounds(mActionBarView.getLeft(), mActionBarView.getTop(),
+ mActionBarView.getRight(), mActionBarView.getBottom());
+ }
+ }
+ setWillNotDraw(mIsSplit ? mSplitBackground == null :
+ mBackground == null && mStackedBackground == null);
+ invalidate();
+ }
+
+ public void setStackedBackground(Drawable bg) {
+ if (mStackedBackground != null) {
+ mStackedBackground.setCallback(null);
+ unscheduleDrawable(mStackedBackground);
+ }
+ mStackedBackground = bg;
+ if (bg != null) {
+ bg.setCallback(this);
+ if ((mIsStacked && mStackedBackground != null)) {
+ mStackedBackground.setBounds(mTabContainer.getLeft(), mTabContainer.getTop(),
+ mTabContainer.getRight(), mTabContainer.getBottom());
+ }
+ }
+ setWillNotDraw(mIsSplit ? mSplitBackground == null :
+ mBackground == null && mStackedBackground == null);
+ invalidate();
+ }
+
+ public void setSplitBackground(Drawable bg) {
+ if (mSplitBackground != null) {
+ mSplitBackground.setCallback(null);
+ unscheduleDrawable(mSplitBackground);
+ }
+ mSplitBackground = bg;
+ if (bg != null) {
+ bg.setCallback(this);
+ if (mIsSplit && mSplitBackground != null) {
+ mSplitBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
+ }
+ }
+ setWillNotDraw(mIsSplit ? mSplitBackground == null :
+ mBackground == null && mStackedBackground == null);
+ invalidate();
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ final boolean isVisible = visibility == VISIBLE;
+ if (mBackground != null) mBackground.setVisible(isVisible, false);
+ if (mStackedBackground != null) mStackedBackground.setVisible(isVisible, false);
+ if (mSplitBackground != null) mSplitBackground.setVisible(isVisible, false);
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return (who == mBackground && !mIsSplit) || (who == mStackedBackground && mIsStacked) ||
+ (who == mSplitBackground && mIsSplit) || super.verifyDrawable(who);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ final int[] state = getDrawableState();
+ boolean changed = false;
+
+ final Drawable background = mBackground;
+ if (background != null && background.isStateful()) {
+ changed |= background.setState(state);
+ }
+
+ final Drawable stackedBackground = mStackedBackground;
+ if (stackedBackground != null && stackedBackground.isStateful()) {
+ changed |= stackedBackground.setState(state);
+ }
+
+ final Drawable splitBackground = mSplitBackground;
+ if (splitBackground != null && splitBackground.isStateful()) {
+ changed |= splitBackground.setState(state);
+ }
+
+ if (changed) {
+ invalidate();
+ }
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mBackground != null) {
+ mBackground.jumpToCurrentState();
+ }
+ if (mStackedBackground != null) {
+ mStackedBackground.jumpToCurrentState();
+ }
+ if (mSplitBackground != null) {
+ mSplitBackground.jumpToCurrentState();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void onResolveDrawables(int layoutDirection) {
+ super.onResolveDrawables(layoutDirection);
+ if (mBackground != null) {
+ mBackground.setLayoutDirection(layoutDirection);
+ }
+ if (mStackedBackground != null) {
+ mStackedBackground.setLayoutDirection(layoutDirection);
+ }
+ if (mSplitBackground != null) {
+ mSplitBackground.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ /**
+ * Set the action bar into a "transitioning" state. While transitioning
+ * the bar will block focus and touch from all of its descendants. This
+ * prevents the user from interacting with the bar while it is animating
+ * in or out.
+ *
+ * @param isTransitioning true if the bar is currently transitioning, false otherwise.
+ */
+ public void setTransitioning(boolean isTransitioning) {
+ mIsTransitioning = isTransitioning;
+ setDescendantFocusability(isTransitioning ? FOCUS_BLOCK_DESCENDANTS
+ : FOCUS_AFTER_DESCENDANTS);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return mIsTransitioning || super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ super.onTouchEvent(ev);
+
+ // An action bar always eats touch events.
+ return true;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent ev) {
+ super.onHoverEvent(ev);
+
+ // An action bar always eats hover events.
+ return true;
+ }
+
+ public void setTabContainer(ScrollingTabContainerView tabView) {
+ if (mTabContainer != null) {
+ removeView(mTabContainer);
+ }
+ mTabContainer = tabView;
+ if (tabView != null) {
+ addView(tabView);
+ final ViewGroup.LayoutParams lp = tabView.getLayoutParams();
+ lp.width = LayoutParams.MATCH_PARENT;
+ lp.height = LayoutParams.WRAP_CONTENT;
+ tabView.setAllowCollapse(false);
+ }
+ }
+
+ public View getTabContainer() {
+ return mTabContainer;
+ }
+
+ @Override
+ public ActionMode startActionModeForChild(
+ View child, ActionMode.Callback callback, int type) {
+ if (type != ActionMode.TYPE_PRIMARY) {
+ return super.startActionModeForChild(child, callback, type);
+ }
+ return null;
+ }
+
+ private static boolean isCollapsed(View view) {
+ return view == null || view.getVisibility() == GONE || view.getMeasuredHeight() == 0;
+ }
+
+ private int getMeasuredHeightWithMargins(View view) {
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ return view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mActionBarView == null &&
+ MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST && mHeight >= 0) {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.min(mHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.AT_MOST);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (mActionBarView == null) return;
+
+ if (mTabContainer != null && mTabContainer.getVisibility() != GONE) {
+ int nonTabMaxHeight = 0;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child == mTabContainer) {
+ continue;
+ }
+ nonTabMaxHeight = Math.max(nonTabMaxHeight, isCollapsed(child) ? 0 :
+ getMeasuredHeightWithMargins(child));
+ }
+ final int mode = MeasureSpec.getMode(heightMeasureSpec);
+ final int maxHeight = mode == MeasureSpec.AT_MOST ?
+ MeasureSpec.getSize(heightMeasureSpec) : Integer.MAX_VALUE;
+ setMeasuredDimension(getMeasuredWidth(),
+ Math.min(nonTabMaxHeight + getMeasuredHeightWithMargins(mTabContainer),
+ maxHeight));
+ }
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ final View tabContainer = mTabContainer;
+ final boolean hasTabs = tabContainer != null && tabContainer.getVisibility() != GONE;
+
+ if (tabContainer != null && tabContainer.getVisibility() != GONE) {
+ final int containerHeight = getMeasuredHeight();
+ final LayoutParams lp = (LayoutParams) tabContainer.getLayoutParams();
+ final int tabHeight = tabContainer.getMeasuredHeight();
+ tabContainer.layout(l, containerHeight - tabHeight - lp.bottomMargin, r,
+ containerHeight - lp.bottomMargin);
+ }
+
+ boolean needsInvalidate = false;
+ if (mIsSplit) {
+ if (mSplitBackground != null) {
+ mSplitBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
+ needsInvalidate = true;
+ }
+ } else {
+ if (mBackground != null) {
+ if (mActionBarView.getVisibility() == View.VISIBLE) {
+ mBackground.setBounds(mActionBarView.getLeft(), mActionBarView.getTop(),
+ mActionBarView.getRight(), mActionBarView.getBottom());
+ } else if (mActionContextView != null &&
+ mActionContextView.getVisibility() == View.VISIBLE) {
+ mBackground.setBounds(mActionContextView.getLeft(), mActionContextView.getTop(),
+ mActionContextView.getRight(), mActionContextView.getBottom());
+ } else {
+ mBackground.setBounds(0, 0, 0, 0);
+ }
+ needsInvalidate = true;
+ }
+ mIsStacked = hasTabs;
+ if (hasTabs && mStackedBackground != null) {
+ mStackedBackground.setBounds(tabContainer.getLeft(), tabContainer.getTop(),
+ tabContainer.getRight(), tabContainer.getBottom());
+ needsInvalidate = true;
+ }
+ }
+
+ if (needsInvalidate) {
+ invalidate();
+ }
+ }
+
+ /**
+ * Dummy drawable so that we don't break background display lists and
+ * projection surfaces.
+ */
+ private class ActionBarBackgroundDrawable extends Drawable {
+ @Override
+ public void draw(Canvas canvas) {
+ if (mIsSplit) {
+ if (mSplitBackground != null) {
+ mSplitBackground.draw(canvas);
+ }
+ } else {
+ if (mBackground != null) {
+ mBackground.draw(canvas);
+ }
+ if (mStackedBackground != null && mIsStacked) {
+ mStackedBackground.draw(canvas);
+ }
+ }
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ if (mIsSplit) {
+ if (mSplitBackground != null) {
+ mSplitBackground.getOutline(outline);
+ }
+ } else {
+ // ignore the stacked background for shadow casting
+ if (mBackground != null) {
+ mBackground.getOutline(outline);
+ }
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ }
+
+ @Override
+ public int getOpacity() {
+ if (mIsSplit) {
+ if (mSplitBackground != null
+ && mSplitBackground.getOpacity() == PixelFormat.OPAQUE) {
+ return PixelFormat.OPAQUE;
+ }
+ } else {
+ if (mIsStacked && (mStackedBackground == null
+ || mStackedBackground.getOpacity() != PixelFormat.OPAQUE)) {
+ return PixelFormat.UNKNOWN;
+ }
+ if (!isCollapsed(mActionBarView) && mBackground != null
+ && mBackground.getOpacity() == PixelFormat.OPAQUE) {
+ return PixelFormat.OPAQUE;
+ }
+ }
+
+ return PixelFormat.UNKNOWN;
+ }
+ }
+}
diff --git a/com/android/internal/widget/ActionBarContextView.java b/com/android/internal/widget/ActionBarContextView.java
new file mode 100644
index 0000000..051526e
--- /dev/null
+++ b/com/android/internal/widget/ActionBarContextView.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ActionMenuPresenter;
+import android.widget.ActionMenuView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.MenuBuilder;
+
+/**
+ * @hide
+ */
+public class ActionBarContextView extends AbsActionBarView {
+ private static final String TAG = "ActionBarContextView";
+
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+
+ private View mClose;
+ private View mCustomView;
+ private LinearLayout mTitleLayout;
+ private TextView mTitleView;
+ private TextView mSubtitleView;
+ private int mTitleStyleRes;
+ private int mSubtitleStyleRes;
+ private Drawable mSplitBackground;
+ private boolean mTitleOptional;
+ private int mCloseItemLayout;
+
+ public ActionBarContextView(Context context) {
+ this(context, null);
+ }
+
+ @UnsupportedAppUsage
+ public ActionBarContextView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.actionModeStyle);
+ }
+
+ public ActionBarContextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ActionBarContextView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ActionMode, defStyleAttr, defStyleRes);
+ setBackground(a.getDrawable(
+ com.android.internal.R.styleable.ActionMode_background));
+ mTitleStyleRes = a.getResourceId(
+ com.android.internal.R.styleable.ActionMode_titleTextStyle, 0);
+ mSubtitleStyleRes = a.getResourceId(
+ com.android.internal.R.styleable.ActionMode_subtitleTextStyle, 0);
+
+ mContentHeight = a.getLayoutDimension(
+ com.android.internal.R.styleable.ActionMode_height, 0);
+
+ mSplitBackground = a.getDrawable(
+ com.android.internal.R.styleable.ActionMode_backgroundSplit);
+
+ mCloseItemLayout = a.getResourceId(
+ com.android.internal.R.styleable.ActionMode_closeItemLayout,
+ R.layout.action_mode_close_item);
+
+ a.recycle();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mActionMenuPresenter != null) {
+ mActionMenuPresenter.hideOverflowMenu();
+ mActionMenuPresenter.hideSubMenus();
+ }
+ }
+
+ @Override
+ public void setSplitToolbar(boolean split) {
+ if (mSplitActionBar != split) {
+ if (mActionMenuPresenter != null) {
+ // Mode is already active; move everything over and adjust the menu itself.
+ final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT);
+ if (!split) {
+ mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+ mMenuView.setBackground(null);
+ final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+ if (oldParent != null) oldParent.removeView(mMenuView);
+ addView(mMenuView, layoutParams);
+ } else {
+ // Allow full screen width in split mode.
+ mActionMenuPresenter.setWidthLimit(
+ getContext().getResources().getDisplayMetrics().widthPixels, true);
+ // No limit to the item count; use whatever will fit.
+ mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+ // Span the whole width
+ layoutParams.width = LayoutParams.MATCH_PARENT;
+ layoutParams.height = mContentHeight;
+ mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+ mMenuView.setBackground(mSplitBackground);
+ final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+ if (oldParent != null) oldParent.removeView(mMenuView);
+ mSplitView.addView(mMenuView, layoutParams);
+ }
+ }
+ super.setSplitToolbar(split);
+ }
+ }
+
+ public void setContentHeight(int height) {
+ mContentHeight = height;
+ }
+
+ public void setCustomView(View view) {
+ if (mCustomView != null) {
+ removeView(mCustomView);
+ }
+ mCustomView = view;
+ if (view != null && mTitleLayout != null) {
+ removeView(mTitleLayout);
+ mTitleLayout = null;
+ }
+ if (view != null) {
+ addView(view);
+ }
+ requestLayout();
+ }
+
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ initTitle();
+ }
+
+ public void setSubtitle(CharSequence subtitle) {
+ mSubtitle = subtitle;
+ initTitle();
+ }
+
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public CharSequence getSubtitle() {
+ return mSubtitle;
+ }
+
+ private void initTitle() {
+ if (mTitleLayout == null) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ inflater.inflate(R.layout.action_bar_title_item, this);
+ mTitleLayout = (LinearLayout) getChildAt(getChildCount() - 1);
+ mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title);
+ mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle);
+ if (mTitleStyleRes != 0) {
+ mTitleView.setTextAppearance(mTitleStyleRes);
+ }
+ if (mSubtitleStyleRes != 0) {
+ mSubtitleView.setTextAppearance(mSubtitleStyleRes);
+ }
+ }
+
+ mTitleView.setText(mTitle);
+ mSubtitleView.setText(mSubtitle);
+
+ final boolean hasTitle = !TextUtils.isEmpty(mTitle);
+ final boolean hasSubtitle = !TextUtils.isEmpty(mSubtitle);
+ mSubtitleView.setVisibility(hasSubtitle ? VISIBLE : GONE);
+ mTitleLayout.setVisibility(hasTitle || hasSubtitle ? VISIBLE : GONE);
+ if (mTitleLayout.getParent() == null) {
+ addView(mTitleLayout);
+ }
+ }
+
+ public void initForMode(final ActionMode mode) {
+ if (mClose == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ mClose = inflater.inflate(mCloseItemLayout, this, false);
+ addView(mClose);
+ } else if (mClose.getParent() == null) {
+ addView(mClose);
+ }
+
+ View closeButton = mClose.findViewById(R.id.action_mode_close_button);
+ closeButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mode.finish();
+ }
+ });
+
+ final MenuBuilder menu = (MenuBuilder) mode.getMenu();
+ if (mActionMenuPresenter != null) {
+ mActionMenuPresenter.dismissPopupMenus();
+ }
+ mActionMenuPresenter = new ActionMenuPresenter(mContext);
+ mActionMenuPresenter.setReserveOverflow(true);
+
+ final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT);
+ if (!mSplitActionBar) {
+ menu.addMenuPresenter(mActionMenuPresenter, mPopupContext);
+ mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+ mMenuView.setBackground(null);
+ addView(mMenuView, layoutParams);
+ } else {
+ // Allow full screen width in split mode.
+ mActionMenuPresenter.setWidthLimit(
+ getContext().getResources().getDisplayMetrics().widthPixels, true);
+ // No limit to the item count; use whatever will fit.
+ mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+ // Span the whole width
+ layoutParams.width = LayoutParams.MATCH_PARENT;
+ layoutParams.height = mContentHeight;
+ menu.addMenuPresenter(mActionMenuPresenter, mPopupContext);
+ mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+ mMenuView.setBackgroundDrawable(mSplitBackground);
+ mSplitView.addView(mMenuView, layoutParams);
+ }
+ }
+
+ public void closeMode() {
+ if (mClose == null) {
+ killMode();
+ return;
+ }
+
+ }
+
+ public void killMode() {
+ removeAllViews();
+ if (mSplitView != null) {
+ mSplitView.removeView(mMenuView);
+ }
+ mCustomView = null;
+ mMenuView = null;
+ }
+
+ @Override
+ public boolean showOverflowMenu() {
+ if (mActionMenuPresenter != null) {
+ return mActionMenuPresenter.showOverflowMenu();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean hideOverflowMenu() {
+ if (mActionMenuPresenter != null) {
+ return mActionMenuPresenter.hideOverflowMenu();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isOverflowMenuShowing() {
+ if (mActionMenuPresenter != null) {
+ return mActionMenuPresenter.isOverflowMenuShowing();
+ }
+ return false;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ // Used by custom views if they don't supply layout params. Everything else
+ // added to an ActionBarContextView should have them already.
+ return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_width=\"match_parent\" (or fill_parent)");
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_height=\"wrap_content\"");
+ }
+
+ final int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
+
+ int maxHeight = mContentHeight > 0 ?
+ mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
+
+ final int verticalPadding = getPaddingTop() + getPaddingBottom();
+ int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight();
+ final int height = maxHeight - verticalPadding;
+ final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+
+ if (mClose != null) {
+ availableWidth = measureChildView(mClose, availableWidth, childSpecHeight, 0);
+ MarginLayoutParams lp = (MarginLayoutParams) mClose.getLayoutParams();
+ availableWidth -= lp.leftMargin + lp.rightMargin;
+ }
+
+ if (mMenuView != null && mMenuView.getParent() == this) {
+ availableWidth = measureChildView(mMenuView, availableWidth,
+ childSpecHeight, 0);
+ }
+
+ if (mTitleLayout != null && mCustomView == null) {
+ if (mTitleOptional) {
+ final int titleWidthSpec = MeasureSpec.makeSafeMeasureSpec(contentWidth,
+ MeasureSpec.UNSPECIFIED);
+ mTitleLayout.measure(titleWidthSpec, childSpecHeight);
+ final int titleWidth = mTitleLayout.getMeasuredWidth();
+ final boolean titleFits = titleWidth <= availableWidth;
+ if (titleFits) {
+ availableWidth -= titleWidth;
+ }
+ mTitleLayout.setVisibility(titleFits ? VISIBLE : GONE);
+ } else {
+ availableWidth = measureChildView(mTitleLayout, availableWidth, childSpecHeight, 0);
+ }
+ }
+
+ if (mCustomView != null) {
+ ViewGroup.LayoutParams lp = mCustomView.getLayoutParams();
+ final int customWidthMode = lp.width != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int customWidth = lp.width >= 0 ?
+ Math.min(lp.width, availableWidth) : availableWidth;
+ final int customHeightMode = lp.height != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int customHeight = lp.height >= 0 ?
+ Math.min(lp.height, height) : height;
+ mCustomView.measure(MeasureSpec.makeMeasureSpec(customWidth, customWidthMode),
+ MeasureSpec.makeMeasureSpec(customHeight, customHeightMode));
+ }
+
+ if (mContentHeight <= 0) {
+ int measuredHeight = 0;
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ View v = getChildAt(i);
+ int paddedViewHeight = v.getMeasuredHeight() + verticalPadding;
+ if (paddedViewHeight > measuredHeight) {
+ measuredHeight = paddedViewHeight;
+ }
+ }
+ setMeasuredDimension(contentWidth, measuredHeight);
+ } else {
+ setMeasuredDimension(contentWidth, maxHeight);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final boolean isLayoutRtl = isLayoutRtl();
+ int x = isLayoutRtl ? r - l - getPaddingRight() : getPaddingLeft();
+ final int y = getPaddingTop();
+ final int contentHeight = b - t - getPaddingTop() - getPaddingBottom();
+
+ if (mClose != null && mClose.getVisibility() != GONE) {
+ MarginLayoutParams lp = (MarginLayoutParams) mClose.getLayoutParams();
+ final int startMargin = (isLayoutRtl ? lp.rightMargin : lp.leftMargin);
+ final int endMargin = (isLayoutRtl ? lp.leftMargin : lp.rightMargin);
+ x = next(x, startMargin, isLayoutRtl);
+ x += positionChild(mClose, x, y, contentHeight, isLayoutRtl);
+ x = next(x, endMargin, isLayoutRtl);
+
+ }
+
+ if (mTitleLayout != null && mCustomView == null && mTitleLayout.getVisibility() != GONE) {
+ x += positionChild(mTitleLayout, x, y, contentHeight, isLayoutRtl);
+ }
+
+ if (mCustomView != null) {
+ x += positionChild(mCustomView, x, y, contentHeight, isLayoutRtl);
+ }
+
+ x = isLayoutRtl ? getPaddingLeft() : r - l - getPaddingRight();
+
+ if (mMenuView != null) {
+ x += positionChild(mMenuView, x, y, contentHeight, !isLayoutRtl);
+ }
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ // Action mode started
+ event.setSource(this);
+ event.setClassName(getClass().getName());
+ event.setPackageName(getContext().getPackageName());
+ event.setContentDescription(mTitle);
+ } else {
+ super.onInitializeAccessibilityEventInternal(event);
+ }
+ }
+
+ public void setTitleOptional(boolean titleOptional) {
+ if (titleOptional != mTitleOptional) {
+ requestLayout();
+ }
+ mTitleOptional = titleOptional;
+ }
+
+ public boolean isTitleOptional() {
+ return mTitleOptional;
+ }
+}
diff --git a/com/android/internal/widget/ActionBarOverlayLayout.java b/com/android/internal/widget/ActionBarOverlayLayout.java
new file mode 100644
index 0000000..aca0b71
--- /dev/null
+++ b/com/android/internal/widget/ActionBarOverlayLayout.java
@@ -0,0 +1,856 @@
+/*
+ * Copyright (C) 2012 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.IntProperty;
+import android.util.Log;
+import android.util.Property;
+import android.util.SparseArray;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.widget.OverScroller;
+import android.widget.Toolbar;
+
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Special layout for the containing of an overlay action bar (and its
+ * content) to correctly handle fitting system windows when the content
+ * has request that its layout ignore them.
+ */
+public class ActionBarOverlayLayout extends ViewGroup implements DecorContentParent {
+ private static final String TAG = "ActionBarOverlayLayout";
+
+ private int mActionBarHeight;
+ //private WindowDecorActionBar mActionBar;
+ private int mWindowVisibility = View.VISIBLE;
+
+ // The main UI elements that we handle the layout of.
+ private View mContent;
+ private ActionBarContainer mActionBarBottom;
+ private ActionBarContainer mActionBarTop;
+
+ // Some interior UI elements.
+ private DecorToolbar mDecorToolbar;
+
+ // Content overlay drawable - generally the action bar's shadow
+ private Drawable mWindowContentOverlay;
+ private boolean mIgnoreWindowContentOverlay;
+
+ private boolean mOverlayMode;
+ private boolean mHasNonEmbeddedTabs;
+ private boolean mHideOnContentScroll;
+ private boolean mAnimatingForFling;
+ private int mHideOnContentScrollReference;
+ private int mLastSystemUiVisibility;
+ private final Rect mBaseContentInsets = new Rect();
+ private final Rect mLastBaseContentInsets = new Rect();
+ private final Rect mContentInsets = new Rect();
+ private WindowInsets mBaseInnerInsets = WindowInsets.CONSUMED;
+ private WindowInsets mLastBaseInnerInsets = WindowInsets.CONSUMED;
+ private WindowInsets mInnerInsets = WindowInsets.CONSUMED;
+ private WindowInsets mLastInnerInsets = WindowInsets.CONSUMED;
+
+ private ActionBarVisibilityCallback mActionBarVisibilityCallback;
+
+ private final int ACTION_BAR_ANIMATE_DELAY = 600; // ms
+
+ private OverScroller mFlingEstimator;
+
+ private ViewPropertyAnimator mCurrentActionBarTopAnimator;
+ private ViewPropertyAnimator mCurrentActionBarBottomAnimator;
+
+ private final Animator.AnimatorListener mTopAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentActionBarTopAnimator = null;
+ mAnimatingForFling = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentActionBarTopAnimator = null;
+ mAnimatingForFling = false;
+ }
+ };
+
+ private final Animator.AnimatorListener mBottomAnimatorListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentActionBarBottomAnimator = null;
+ mAnimatingForFling = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentActionBarBottomAnimator = null;
+ mAnimatingForFling = false;
+ }
+ };
+
+ private final Runnable mRemoveActionBarHideOffset = new Runnable() {
+ public void run() {
+ haltActionBarHideOffsetAnimations();
+ mCurrentActionBarTopAnimator = mActionBarTop.animate().translationY(0)
+ .setListener(mTopAnimatorListener);
+ if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) {
+ mCurrentActionBarBottomAnimator = mActionBarBottom.animate().translationY(0)
+ .setListener(mBottomAnimatorListener);
+ }
+ }
+ };
+
+ private final Runnable mAddActionBarHideOffset = new Runnable() {
+ public void run() {
+ haltActionBarHideOffsetAnimations();
+ mCurrentActionBarTopAnimator = mActionBarTop.animate()
+ .translationY(-mActionBarTop.getHeight())
+ .setListener(mTopAnimatorListener);
+ if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) {
+ mCurrentActionBarBottomAnimator = mActionBarBottom.animate()
+ .translationY(mActionBarBottom.getHeight())
+ .setListener(mBottomAnimatorListener);
+ }
+ }
+ };
+
+ public static final Property<ActionBarOverlayLayout, Integer> ACTION_BAR_HIDE_OFFSET =
+ new IntProperty<ActionBarOverlayLayout>("actionBarHideOffset") {
+
+ @Override
+ public void setValue(ActionBarOverlayLayout object, int value) {
+ object.setActionBarHideOffset(value);
+ }
+
+ @Override
+ public Integer get(ActionBarOverlayLayout object) {
+ return object.getActionBarHideOffset();
+ }
+ };
+
+ static final int[] ATTRS = new int [] {
+ com.android.internal.R.attr.actionBarSize,
+ com.android.internal.R.attr.windowContentOverlay
+ };
+
+ public ActionBarOverlayLayout(Context context) {
+ super(context);
+ init(context);
+ }
+
+ @UnsupportedAppUsage
+ public ActionBarOverlayLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ TypedArray ta = getContext().getTheme().obtainStyledAttributes(ATTRS);
+ mActionBarHeight = ta.getDimensionPixelSize(0, 0);
+ mWindowContentOverlay = ta.getDrawable(1);
+ setWillNotDraw(mWindowContentOverlay == null);
+ ta.recycle();
+
+ mIgnoreWindowContentOverlay = context.getApplicationInfo().targetSdkVersion <
+ Build.VERSION_CODES.KITKAT;
+
+ mFlingEstimator = new OverScroller(context);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ haltActionBarHideOffsetAnimations();
+ }
+
+ public void setActionBarVisibilityCallback(ActionBarVisibilityCallback cb) {
+ mActionBarVisibilityCallback = cb;
+ if (getWindowToken() != null) {
+ // This is being initialized after being added to a window;
+ // make sure to update all state now.
+ mActionBarVisibilityCallback.onWindowVisibilityChanged(mWindowVisibility);
+ if (mLastSystemUiVisibility != 0) {
+ int newVis = mLastSystemUiVisibility;
+ onWindowSystemUiVisibilityChanged(newVis);
+ requestApplyInsets();
+ }
+ }
+ }
+
+ public void setOverlayMode(boolean overlayMode) {
+ mOverlayMode = overlayMode;
+
+ /*
+ * Drawing the window content overlay was broken before K so starting to draw it
+ * again unexpectedly will cause artifacts in some apps. They should fix it.
+ */
+ mIgnoreWindowContentOverlay = overlayMode &&
+ getContext().getApplicationInfo().targetSdkVersion <
+ Build.VERSION_CODES.KITKAT;
+ }
+
+ public boolean isInOverlayMode() {
+ return mOverlayMode;
+ }
+
+ public void setHasNonEmbeddedTabs(boolean hasNonEmbeddedTabs) {
+ mHasNonEmbeddedTabs = hasNonEmbeddedTabs;
+ }
+
+ public void setShowingForActionMode(boolean showing) {
+ if (showing) {
+ // Here's a fun hack: if the status bar is currently being hidden,
+ // and the application has asked for stable content insets, then
+ // we will end up with the action mode action bar being shown
+ // without the status bar, but moved below where the status bar
+ // would be. Not nice. Trying to have this be positioned
+ // correctly is not easy (basically we need yet *another* content
+ // inset from the window manager to know where to put it), so
+ // instead we will just temporarily force the status bar to be shown.
+ if ((getWindowSystemUiVisibility() & (SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | SYSTEM_UI_FLAG_LAYOUT_STABLE))
+ == (SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE)) {
+ setDisabledSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN);
+ }
+ } else {
+ setDisabledSystemUiVisibility(0);
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ init(getContext());
+ requestApplyInsets();
+ }
+
+ @Override
+ public void onWindowSystemUiVisibilityChanged(int visible) {
+ super.onWindowSystemUiVisibilityChanged(visible);
+ pullChildren();
+ final int diff = mLastSystemUiVisibility ^ visible;
+ mLastSystemUiVisibility = visible;
+ final boolean barVisible = (visible & SYSTEM_UI_FLAG_FULLSCREEN) == 0;
+ final boolean stable = (visible & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0;
+ if (mActionBarVisibilityCallback != null) {
+ // We want the bar to be visible if it is not being hidden,
+ // or the app has not turned on a stable UI mode (meaning they
+ // are performing explicit layout around the action bar).
+ mActionBarVisibilityCallback.enableContentAnimations(!stable);
+ if (barVisible || !stable) mActionBarVisibilityCallback.showForSystem();
+ else mActionBarVisibilityCallback.hideForSystem();
+ }
+ if ((diff & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) {
+ if (mActionBarVisibilityCallback != null) {
+ requestApplyInsets();
+ }
+ }
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mWindowVisibility = visibility;
+ if (mActionBarVisibilityCallback != null) {
+ mActionBarVisibilityCallback.onWindowVisibilityChanged(visibility);
+ }
+ }
+
+ private boolean applyInsets(View view, Rect insets, boolean left, boolean top,
+ boolean bottom, boolean right) {
+ boolean changed = false;
+ LayoutParams lp = (LayoutParams)view.getLayoutParams();
+ if (left && lp.leftMargin != insets.left) {
+ changed = true;
+ lp.leftMargin = insets.left;
+ }
+ if (top && lp.topMargin != insets.top) {
+ changed = true;
+ lp.topMargin = insets.top;
+ }
+ if (right && lp.rightMargin != insets.right) {
+ changed = true;
+ lp.rightMargin = insets.right;
+ }
+ if (bottom && lp.bottomMargin != insets.bottom) {
+ changed = true;
+ lp.bottomMargin = insets.bottom;
+ }
+ return changed;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ pullChildren();
+
+ final int vis = getWindowSystemUiVisibility();
+ final Rect systemInsets = insets.getSystemWindowInsetsAsRect();
+
+ // The top and bottom action bars are always within the content area.
+ boolean changed = applyInsets(mActionBarTop, systemInsets, true, true, false, true);
+ if (mActionBarBottom != null) {
+ changed |= applyInsets(mActionBarBottom, systemInsets, true, false, true, true);
+ }
+
+ // Cannot use the result of computeSystemWindowInsets, because that consumes the
+ // systemWindowInsets. Instead, we do the insetting by the local insets ourselves.
+ computeSystemWindowInsets(insets, mBaseContentInsets);
+ mBaseInnerInsets = insets.inset(mBaseContentInsets);
+
+ if (!mLastBaseInnerInsets.equals(mBaseInnerInsets)) {
+ changed = true;
+ mLastBaseInnerInsets = mBaseInnerInsets;
+ }
+ if (!mLastBaseContentInsets.equals(mBaseContentInsets)) {
+ changed = true;
+ mLastBaseContentInsets.set(mBaseContentInsets);
+ }
+
+ if (changed) {
+ requestLayout();
+ }
+
+ // We don't do any more at this point. To correctly compute the content/inner
+ // insets in all cases, we need to know the measured size of the various action
+ // bar elements. onApplyWindowInsets() happens before the measure pass, so we can't
+ // do that here. Instead we will take this up in onMeasure().
+ return WindowInsets.CONSUMED;
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ pullChildren();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ int topInset = 0;
+ int bottomInset = 0;
+
+ measureChildWithMargins(mActionBarTop, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ LayoutParams lp = (LayoutParams) mActionBarTop.getLayoutParams();
+ maxWidth = Math.max(maxWidth,
+ mActionBarTop.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight,
+ mActionBarTop.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+ childState = combineMeasuredStates(childState, mActionBarTop.getMeasuredState());
+
+ // xlarge screen layout doesn't have bottom action bar.
+ if (mActionBarBottom != null) {
+ measureChildWithMargins(mActionBarBottom, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ lp = (LayoutParams) mActionBarBottom.getLayoutParams();
+ maxWidth = Math.max(maxWidth,
+ mActionBarBottom.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight,
+ mActionBarBottom.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+ childState = combineMeasuredStates(childState, mActionBarBottom.getMeasuredState());
+ }
+
+ final int vis = getWindowSystemUiVisibility();
+ final boolean stable = (vis & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0;
+
+ if (stable) {
+ // This is the standard space needed for the action bar. For stable measurement,
+ // we can't depend on the size currently reported by it -- this must remain constant.
+ topInset = mActionBarHeight;
+ if (mHasNonEmbeddedTabs) {
+ final View tabs = mActionBarTop.getTabContainer();
+ if (tabs != null) {
+ // If tabs are not embedded, increase space on top to account for them.
+ topInset += mActionBarHeight;
+ }
+ }
+ } else if (mActionBarTop.getVisibility() != GONE) {
+ // This is the space needed on top of the window for all of the action bar
+ // and tabs.
+ topInset = mActionBarTop.getMeasuredHeight();
+ }
+
+ if (mDecorToolbar.isSplit()) {
+ // If action bar is split, adjust bottom insets for it.
+ if (mActionBarBottom != null) {
+ if (stable) {
+ bottomInset = mActionBarHeight;
+ } else {
+ bottomInset = mActionBarBottom.getMeasuredHeight();
+ }
+ }
+ }
+
+ // If the window has not requested system UI layout flags, we need to
+ // make sure its content is not being covered by system UI... though it
+ // will still be covered by the action bar if they have requested it to
+ // overlay.
+ mContentInsets.set(mBaseContentInsets);
+ mInnerInsets = mBaseInnerInsets;
+ if (!mOverlayMode && !stable) {
+ mContentInsets.top += topInset;
+ mContentInsets.bottom += bottomInset;
+ // Content view has been shrunk, shrink all insets to match.
+ mInnerInsets = mInnerInsets.inset(0 /* left */, topInset, 0 /* right */, bottomInset);
+ } else {
+ // Add ActionBar to system window inset, but leave other insets untouched.
+ mInnerInsets = mInnerInsets.replaceSystemWindowInsets(
+ mInnerInsets.getSystemWindowInsetLeft(),
+ mInnerInsets.getSystemWindowInsetTop() + topInset,
+ mInnerInsets.getSystemWindowInsetRight(),
+ mInnerInsets.getSystemWindowInsetBottom() + bottomInset
+ );
+ }
+ applyInsets(mContent, mContentInsets, true, true, true, true);
+
+ if (!mLastInnerInsets.equals(mInnerInsets)) {
+ // If the inner insets have changed, we need to dispatch this down to
+ // the app's onApplyWindowInsets(). We do this before measuring the content
+ // view to keep the same semantics as the normal fitSystemWindows() call.
+ mLastInnerInsets = mInnerInsets;
+ mContent.dispatchApplyWindowInsets(mInnerInsets);
+ }
+
+ measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ lp = (LayoutParams) mContent.getLayoutParams();
+ maxWidth = Math.max(maxWidth,
+ mContent.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight,
+ mContent.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+ childState = combineMeasuredStates(childState, mContent.getMeasuredState());
+
+ // Account for padding too
+ maxWidth += getPaddingLeft() + getPaddingRight();
+ maxHeight += getPaddingTop() + getPaddingBottom();
+
+ // Check against our minimum height and width
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int count = getChildCount();
+
+ final int parentLeft = getPaddingLeft();
+ final int parentRight = right - left - getPaddingRight();
+
+ final int parentTop = getPaddingTop();
+ final int parentBottom = bottom - top - getPaddingBottom();
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final int width = child.getMeasuredWidth();
+ final int height = child.getMeasuredHeight();
+
+ int childLeft = parentLeft + lp.leftMargin;
+ int childTop;
+ if (child == mActionBarBottom) {
+ childTop = parentBottom - height - lp.bottomMargin;
+ } else {
+ childTop = parentTop + lp.topMargin;
+ }
+
+ child.layout(childLeft, childTop, childLeft + width, childTop + height);
+ }
+ }
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ super.draw(c);
+ if (mWindowContentOverlay != null && !mIgnoreWindowContentOverlay) {
+ final int top = mActionBarTop.getVisibility() == VISIBLE ?
+ (int) (mActionBarTop.getBottom() + mActionBarTop.getTranslationY() + 0.5f) : 0;
+ mWindowContentOverlay.setBounds(0, top, getWidth(),
+ top + mWindowContentOverlay.getIntrinsicHeight());
+ mWindowContentOverlay.draw(c);
+ }
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int axes) {
+ if ((axes & SCROLL_AXIS_VERTICAL) == 0 || mActionBarTop.getVisibility() != VISIBLE) {
+ return false;
+ }
+ return mHideOnContentScroll;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ super.onNestedScrollAccepted(child, target, axes);
+ mHideOnContentScrollReference = getActionBarHideOffset();
+ haltActionBarHideOffsetAnimations();
+ if (mActionBarVisibilityCallback != null) {
+ mActionBarVisibilityCallback.onContentScrollStarted();
+ }
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ mHideOnContentScrollReference += dyConsumed;
+ setActionBarHideOffset(mHideOnContentScrollReference);
+ }
+
+ @Override
+ public void onStopNestedScroll(View target) {
+ super.onStopNestedScroll(target);
+ if (mHideOnContentScroll && !mAnimatingForFling) {
+ if (mHideOnContentScrollReference <= mActionBarTop.getHeight()) {
+ postRemoveActionBarHideOffset();
+ } else {
+ postAddActionBarHideOffset();
+ }
+ }
+ if (mActionBarVisibilityCallback != null) {
+ mActionBarVisibilityCallback.onContentScrollStopped();
+ }
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (!mHideOnContentScroll || !consumed) {
+ return false;
+ }
+ if (shouldHideActionBarOnFling(velocityX, velocityY)) {
+ addActionBarHideOffset();
+ } else {
+ removeActionBarHideOffset();
+ }
+ mAnimatingForFling = true;
+ return true;
+ }
+
+ void pullChildren() {
+ if (mContent == null) {
+ mContent = findViewById(com.android.internal.R.id.content);
+ mActionBarTop = findViewById(
+ com.android.internal.R.id.action_bar_container);
+ mDecorToolbar = getDecorToolbar(findViewById(com.android.internal.R.id.action_bar));
+ mActionBarBottom = findViewById(
+ com.android.internal.R.id.split_action_bar);
+ }
+ }
+
+ private DecorToolbar getDecorToolbar(View view) {
+ if (view instanceof DecorToolbar) {
+ return (DecorToolbar) view;
+ } else if (view instanceof Toolbar) {
+ return ((Toolbar) view).getWrapper();
+ } else {
+ throw new IllegalStateException("Can't make a decor toolbar out of " +
+ view.getClass().getSimpleName());
+ }
+ }
+
+ public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) {
+ if (hideOnContentScroll != mHideOnContentScroll) {
+ mHideOnContentScroll = hideOnContentScroll;
+ if (!hideOnContentScroll) {
+ stopNestedScroll();
+ haltActionBarHideOffsetAnimations();
+ setActionBarHideOffset(0);
+ }
+ }
+ }
+
+ public boolean isHideOnContentScrollEnabled() {
+ return mHideOnContentScroll;
+ }
+
+ public int getActionBarHideOffset() {
+ return mActionBarTop != null ? -((int) mActionBarTop.getTranslationY()) : 0;
+ }
+
+ public void setActionBarHideOffset(int offset) {
+ haltActionBarHideOffsetAnimations();
+ final int topHeight = mActionBarTop.getHeight();
+ offset = Math.max(0, Math.min(offset, topHeight));
+ mActionBarTop.setTranslationY(-offset);
+ if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) {
+ // Match the hide offset proportionally for a split bar
+ final float fOffset = (float) offset / topHeight;
+ final int bOffset = (int) (mActionBarBottom.getHeight() * fOffset);
+ mActionBarBottom.setTranslationY(bOffset);
+ }
+ }
+
+ private void haltActionBarHideOffsetAnimations() {
+ removeCallbacks(mRemoveActionBarHideOffset);
+ removeCallbacks(mAddActionBarHideOffset);
+ if (mCurrentActionBarTopAnimator != null) {
+ mCurrentActionBarTopAnimator.cancel();
+ }
+ if (mCurrentActionBarBottomAnimator != null) {
+ mCurrentActionBarBottomAnimator.cancel();
+ }
+ }
+
+ private void postRemoveActionBarHideOffset() {
+ haltActionBarHideOffsetAnimations();
+ postDelayed(mRemoveActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY);
+ }
+
+ private void postAddActionBarHideOffset() {
+ haltActionBarHideOffsetAnimations();
+ postDelayed(mAddActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY);
+ }
+
+ private void removeActionBarHideOffset() {
+ haltActionBarHideOffsetAnimations();
+ mRemoveActionBarHideOffset.run();
+ }
+
+ private void addActionBarHideOffset() {
+ haltActionBarHideOffsetAnimations();
+ mAddActionBarHideOffset.run();
+ }
+
+ private boolean shouldHideActionBarOnFling(float velocityX, float velocityY) {
+ mFlingEstimator.fling(0, 0, 0, (int) velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
+ final int finalY = mFlingEstimator.getFinalY();
+ return finalY > mActionBarTop.getHeight();
+ }
+
+ @UnsupportedAppUsage
+ @Override
+ public void setWindowCallback(Window.Callback cb) {
+ pullChildren();
+ mDecorToolbar.setWindowCallback(cb);
+ }
+
+ @Override
+ public void setWindowTitle(CharSequence title) {
+ pullChildren();
+ mDecorToolbar.setWindowTitle(title);
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ pullChildren();
+ return mDecorToolbar.getTitle();
+ }
+
+ @Override
+ public void initFeature(int windowFeature) {
+ pullChildren();
+ switch (windowFeature) {
+ case Window.FEATURE_PROGRESS:
+ mDecorToolbar.initProgress();
+ break;
+ case Window.FEATURE_INDETERMINATE_PROGRESS:
+ mDecorToolbar.initIndeterminateProgress();
+ break;
+ case Window.FEATURE_ACTION_BAR_OVERLAY:
+ setOverlayMode(true);
+ break;
+ }
+ }
+
+ @Override
+ public void setUiOptions(int uiOptions) {
+ boolean splitActionBar = false;
+ final boolean splitWhenNarrow =
+ (uiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;
+ if (splitWhenNarrow) {
+ splitActionBar = getContext().getResources().getBoolean(
+ com.android.internal.R.bool.split_action_bar_is_narrow);
+ }
+ if (splitActionBar) {
+ pullChildren();
+ if (mActionBarBottom != null && mDecorToolbar.canSplit()) {
+ mDecorToolbar.setSplitView(mActionBarBottom);
+ mDecorToolbar.setSplitToolbar(splitActionBar);
+ mDecorToolbar.setSplitWhenNarrow(splitWhenNarrow);
+
+ final ActionBarContextView cab = findViewById(
+ com.android.internal.R.id.action_context_bar);
+ cab.setSplitView(mActionBarBottom);
+ cab.setSplitToolbar(splitActionBar);
+ cab.setSplitWhenNarrow(splitWhenNarrow);
+ } else if (splitActionBar) {
+ Log.e(TAG, "Requested split action bar with " +
+ "incompatible window decor! Ignoring request.");
+ }
+ }
+ }
+
+ @Override
+ public boolean hasIcon() {
+ pullChildren();
+ return mDecorToolbar.hasIcon();
+ }
+
+ @Override
+ public boolean hasLogo() {
+ pullChildren();
+ return mDecorToolbar.hasLogo();
+ }
+
+ @Override
+ public void setIcon(int resId) {
+ pullChildren();
+ mDecorToolbar.setIcon(resId);
+ }
+
+ @Override
+ public void setIcon(Drawable d) {
+ pullChildren();
+ mDecorToolbar.setIcon(d);
+ }
+
+ @Override
+ public void setLogo(int resId) {
+ pullChildren();
+ mDecorToolbar.setLogo(resId);
+ }
+
+ @Override
+ public boolean canShowOverflowMenu() {
+ pullChildren();
+ return mDecorToolbar.canShowOverflowMenu();
+ }
+
+ @Override
+ public boolean isOverflowMenuShowing() {
+ pullChildren();
+ return mDecorToolbar.isOverflowMenuShowing();
+ }
+
+ @Override
+ public boolean isOverflowMenuShowPending() {
+ pullChildren();
+ return mDecorToolbar.isOverflowMenuShowPending();
+ }
+
+ @Override
+ public boolean showOverflowMenu() {
+ pullChildren();
+ return mDecorToolbar.showOverflowMenu();
+ }
+
+ @Override
+ public boolean hideOverflowMenu() {
+ pullChildren();
+ return mDecorToolbar.hideOverflowMenu();
+ }
+
+ @Override
+ public void setMenuPrepared() {
+ pullChildren();
+ mDecorToolbar.setMenuPrepared();
+ }
+
+ @Override
+ public void setMenu(Menu menu, MenuPresenter.Callback cb) {
+ pullChildren();
+ mDecorToolbar.setMenu(menu, cb);
+ }
+
+ @Override
+ public void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates) {
+ pullChildren();
+ mDecorToolbar.saveHierarchyState(toolbarStates);
+ }
+
+ @Override
+ public void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates) {
+ pullChildren();
+ mDecorToolbar.restoreHierarchyState(toolbarStates);
+ }
+
+ @Override
+ public void dismissPopups() {
+ pullChildren();
+ mDecorToolbar.dismissPopupMenus();
+ }
+
+ public static class LayoutParams extends MarginLayoutParams {
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+ }
+
+ public interface ActionBarVisibilityCallback {
+ void onWindowVisibilityChanged(int visibility);
+ void showForSystem();
+ void hideForSystem();
+ void enableContentAnimations(boolean enable);
+ void onContentScrollStarted();
+ void onContentScrollStopped();
+ }
+}
diff --git a/com/android/internal/widget/ActionBarView.java b/com/android/internal/widget/ActionBarView.java
new file mode 100644
index 0000000..f90b59d
--- /dev/null
+++ b/com/android/internal/widget/ActionBarView.java
@@ -0,0 +1,1730 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.animation.LayoutTransition;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.CollapsibleActionView;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.Window;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ActionMenuPresenter;
+import android.widget.ActionMenuView;
+import android.widget.AdapterView;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+import com.android.internal.R;
+import com.android.internal.view.menu.ActionMenuItem;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.view.menu.MenuView;
+import com.android.internal.view.menu.SubMenuBuilder;
+
+/**
+ * @hide
+ */
+public class ActionBarView extends AbsActionBarView implements DecorToolbar {
+ private static final String TAG = "ActionBarView";
+
+ /**
+ * Display options applied by default
+ */
+ public static final int DISPLAY_DEFAULT = 0;
+
+ /**
+ * Display options that require re-layout as opposed to a simple invalidate
+ */
+ private static final int DISPLAY_RELAYOUT_MASK =
+ ActionBar.DISPLAY_SHOW_HOME |
+ ActionBar.DISPLAY_USE_LOGO |
+ ActionBar.DISPLAY_HOME_AS_UP |
+ ActionBar.DISPLAY_SHOW_CUSTOM |
+ ActionBar.DISPLAY_SHOW_TITLE |
+ ActionBar.DISPLAY_TITLE_MULTIPLE_LINES;
+
+ private static final int DEFAULT_CUSTOM_GRAVITY = Gravity.START | Gravity.CENTER_VERTICAL;
+
+ private int mNavigationMode;
+ private int mDisplayOptions = -1;
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+ private Drawable mIcon;
+ private Drawable mLogo;
+ private CharSequence mHomeDescription;
+ private int mHomeDescriptionRes;
+
+ private HomeView mHomeLayout;
+ private HomeView mExpandedHomeLayout;
+ private LinearLayout mTitleLayout;
+ private TextView mTitleView;
+ private TextView mSubtitleView;
+ private ViewGroup mUpGoerFive;
+
+ private Spinner mSpinner;
+ private LinearLayout mListNavLayout;
+ private ScrollingTabContainerView mTabScrollView;
+ private View mCustomNavView;
+ private ProgressBar mProgressView;
+ private ProgressBar mIndeterminateProgressView;
+
+ private int mProgressBarPadding;
+ private int mItemPadding;
+
+ private final int mTitleStyleRes;
+ private final int mSubtitleStyleRes;
+ private final int mProgressStyle;
+ private final int mIndeterminateProgressStyle;
+
+ private boolean mUserTitle;
+ private boolean mIncludeTabs;
+ private boolean mIsCollapsible;
+ private boolean mWasHomeEnabled; // Was it enabled before action view expansion?
+
+ private MenuBuilder mOptionsMenu;
+ private boolean mMenuPrepared;
+
+ private ActionBarContextView mContextView;
+
+ private ActionMenuItem mLogoNavItem;
+
+ private SpinnerAdapter mSpinnerAdapter;
+ private AdapterView.OnItemSelectedListener mNavItemSelectedListener;
+
+ private Runnable mTabSelector;
+
+ private ExpandedActionViewMenuPresenter mExpandedMenuPresenter;
+ View mExpandedActionView;
+ private int mDefaultUpDescription = R.string.action_bar_up_description;
+
+ Window.Callback mWindowCallback;
+
+ private final OnClickListener mExpandedActionViewUpListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final MenuItemImpl item = mExpandedMenuPresenter.mCurrentExpandedItem;
+ if (item != null) {
+ item.collapseActionView();
+ }
+ }
+ };
+
+ private final OnClickListener mUpClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (mMenuPrepared) {
+ // Only invoke the window callback if the options menu has been initialized.
+ mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, mLogoNavItem);
+ }
+ }
+ };
+
+ public ActionBarView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Background is always provided by the container.
+ setBackgroundResource(0);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar,
+ com.android.internal.R.attr.actionBarStyle, 0);
+
+ mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode,
+ ActionBar.NAVIGATION_MODE_STANDARD);
+ mTitle = a.getText(R.styleable.ActionBar_title);
+ mSubtitle = a.getText(R.styleable.ActionBar_subtitle);
+ mLogo = a.getDrawable(R.styleable.ActionBar_logo);
+ mIcon = a.getDrawable(R.styleable.ActionBar_icon);
+
+ final LayoutInflater inflater = LayoutInflater.from(context);
+
+ final int homeResId = a.getResourceId(
+ com.android.internal.R.styleable.ActionBar_homeLayout,
+ com.android.internal.R.layout.action_bar_home);
+
+ mUpGoerFive = (ViewGroup) inflater.inflate(
+ com.android.internal.R.layout.action_bar_up_container, this, false);
+ mHomeLayout = (HomeView) inflater.inflate(homeResId, mUpGoerFive, false);
+
+ mExpandedHomeLayout = (HomeView) inflater.inflate(homeResId, mUpGoerFive, false);
+ mExpandedHomeLayout.setShowUp(true);
+ mExpandedHomeLayout.setOnClickListener(mExpandedActionViewUpListener);
+ mExpandedHomeLayout.setContentDescription(getResources().getText(
+ mDefaultUpDescription));
+
+ // This needs to highlight/be focusable on its own.
+ // TODO: Clean up the handoff between expanded/normal.
+ final Drawable upBackground = mUpGoerFive.getBackground();
+ if (upBackground != null) {
+ mExpandedHomeLayout.setBackground(upBackground.getConstantState().newDrawable());
+ }
+ mExpandedHomeLayout.setEnabled(true);
+ mExpandedHomeLayout.setFocusable(true);
+
+ mTitleStyleRes = a.getResourceId(R.styleable.ActionBar_titleTextStyle, 0);
+ mSubtitleStyleRes = a.getResourceId(R.styleable.ActionBar_subtitleTextStyle, 0);
+ mProgressStyle = a.getResourceId(R.styleable.ActionBar_progressBarStyle, 0);
+ mIndeterminateProgressStyle = a.getResourceId(
+ R.styleable.ActionBar_indeterminateProgressStyle, 0);
+
+ mProgressBarPadding = a.getDimensionPixelOffset(R.styleable.ActionBar_progressBarPadding, 0);
+ mItemPadding = a.getDimensionPixelOffset(R.styleable.ActionBar_itemPadding, 0);
+
+ setDisplayOptions(a.getInt(R.styleable.ActionBar_displayOptions, DISPLAY_DEFAULT));
+
+ final int customNavId = a.getResourceId(R.styleable.ActionBar_customNavigationLayout, 0);
+ if (customNavId != 0) {
+ mCustomNavView = (View) inflater.inflate(customNavId, this, false);
+ mNavigationMode = ActionBar.NAVIGATION_MODE_STANDARD;
+ setDisplayOptions(mDisplayOptions | ActionBar.DISPLAY_SHOW_CUSTOM);
+ }
+
+ mContentHeight = a.getLayoutDimension(R.styleable.ActionBar_height, 0);
+
+ a.recycle();
+
+ mLogoNavItem = new ActionMenuItem(context, 0, android.R.id.home, 0, 0, mTitle);
+
+ mUpGoerFive.setOnClickListener(mUpClickListener);
+ mUpGoerFive.setClickable(true);
+ mUpGoerFive.setFocusable(true);
+
+ if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ mTitleView = null;
+ mSubtitleView = null;
+ if (mTitleLayout != null && mTitleLayout.getParent() == mUpGoerFive) {
+ mUpGoerFive.removeView(mTitleLayout);
+ }
+ mTitleLayout = null;
+ if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ initTitle();
+ }
+
+ if (mHomeDescriptionRes != 0) {
+ setNavigationContentDescription(mHomeDescriptionRes);
+ }
+
+ if (mTabScrollView != null && mIncludeTabs) {
+ ViewGroup.LayoutParams lp = mTabScrollView.getLayoutParams();
+ if (lp != null) {
+ lp.width = LayoutParams.WRAP_CONTENT;
+ lp.height = LayoutParams.MATCH_PARENT;
+ }
+ mTabScrollView.setAllowCollapse(true);
+ }
+ }
+
+ /**
+ * Set the window callback used to invoke menu items; used for dispatching home button presses.
+ * @param cb Window callback to dispatch to
+ */
+ public void setWindowCallback(Window.Callback cb) {
+ mWindowCallback = cb;
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ removeCallbacks(mTabSelector);
+ if (mActionMenuPresenter != null) {
+ mActionMenuPresenter.hideOverflowMenu();
+ mActionMenuPresenter.hideSubMenus();
+ }
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ public void initProgress() {
+ mProgressView = new ProgressBar(mContext, null, 0, mProgressStyle);
+ mProgressView.setId(R.id.progress_horizontal);
+ mProgressView.setMax(10000);
+ mProgressView.setVisibility(GONE);
+ addView(mProgressView);
+ }
+
+ public void initIndeterminateProgress() {
+ mIndeterminateProgressView = new ProgressBar(mContext, null, 0,
+ mIndeterminateProgressStyle);
+ mIndeterminateProgressView.setId(R.id.progress_circular);
+ mIndeterminateProgressView.setVisibility(GONE);
+ addView(mIndeterminateProgressView);
+ }
+
+ @Override
+ public void setSplitToolbar(boolean splitActionBar) {
+ if (mSplitActionBar != splitActionBar) {
+ if (mMenuView != null) {
+ final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+ if (oldParent != null) {
+ oldParent.removeView(mMenuView);
+ }
+ if (splitActionBar) {
+ if (mSplitView != null) {
+ mSplitView.addView(mMenuView);
+ }
+ mMenuView.getLayoutParams().width = LayoutParams.MATCH_PARENT;
+ } else {
+ addView(mMenuView);
+ mMenuView.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
+ }
+ mMenuView.requestLayout();
+ }
+ if (mSplitView != null) {
+ mSplitView.setVisibility(splitActionBar ? VISIBLE : GONE);
+ }
+
+ if (mActionMenuPresenter != null) {
+ if (!splitActionBar) {
+ mActionMenuPresenter.setExpandedActionViewsExclusive(
+ getResources().getBoolean(
+ com.android.internal.R.bool.action_bar_expanded_action_views_exclusive));
+ } else {
+ mActionMenuPresenter.setExpandedActionViewsExclusive(false);
+ // Allow full screen width in split mode.
+ mActionMenuPresenter.setWidthLimit(
+ getContext().getResources().getDisplayMetrics().widthPixels, true);
+ // No limit to the item count; use whatever will fit.
+ mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+ }
+ }
+ super.setSplitToolbar(splitActionBar);
+ }
+ }
+
+ public boolean isSplit() {
+ return mSplitActionBar;
+ }
+
+ public boolean canSplit() {
+ return true;
+ }
+
+ public boolean hasEmbeddedTabs() {
+ return mIncludeTabs;
+ }
+
+ @Override
+ public void setEmbeddedTabView(ScrollingTabContainerView tabs) {
+ if (mTabScrollView != null) {
+ removeView(mTabScrollView);
+ }
+ mTabScrollView = tabs;
+ mIncludeTabs = tabs != null;
+ if (mIncludeTabs && mNavigationMode == ActionBar.NAVIGATION_MODE_TABS) {
+ addView(mTabScrollView);
+ ViewGroup.LayoutParams lp = mTabScrollView.getLayoutParams();
+ lp.width = LayoutParams.WRAP_CONTENT;
+ lp.height = LayoutParams.MATCH_PARENT;
+ tabs.setAllowCollapse(true);
+ }
+ }
+
+ public void setMenuPrepared() {
+ mMenuPrepared = true;
+ }
+
+ public void setMenu(Menu menu, MenuPresenter.Callback cb) {
+ if (menu == mOptionsMenu) return;
+
+ if (mOptionsMenu != null) {
+ mOptionsMenu.removeMenuPresenter(mActionMenuPresenter);
+ mOptionsMenu.removeMenuPresenter(mExpandedMenuPresenter);
+ }
+
+ MenuBuilder builder = (MenuBuilder) menu;
+ mOptionsMenu = builder;
+ if (mMenuView != null) {
+ final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
+ if (oldParent != null) {
+ oldParent.removeView(mMenuView);
+ }
+ }
+ if (mActionMenuPresenter == null) {
+ mActionMenuPresenter = new ActionMenuPresenter(mContext);
+ mActionMenuPresenter.setCallback(cb);
+ mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter);
+ mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
+ }
+
+ ActionMenuView menuView;
+ final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT);
+ if (!mSplitActionBar) {
+ mActionMenuPresenter.setExpandedActionViewsExclusive(
+ getResources().getBoolean(
+ com.android.internal.R.bool.action_bar_expanded_action_views_exclusive));
+ configPresenters(builder);
+ menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+ final ViewGroup oldParent = (ViewGroup) menuView.getParent();
+ if (oldParent != null && oldParent != this) {
+ oldParent.removeView(menuView);
+ }
+ addView(menuView, layoutParams);
+ } else {
+ mActionMenuPresenter.setExpandedActionViewsExclusive(false);
+ // Allow full screen width in split mode.
+ mActionMenuPresenter.setWidthLimit(
+ getContext().getResources().getDisplayMetrics().widthPixels, true);
+ // No limit to the item count; use whatever will fit.
+ mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
+ // Span the whole width
+ layoutParams.width = LayoutParams.MATCH_PARENT;
+ layoutParams.height = LayoutParams.WRAP_CONTENT;
+ configPresenters(builder);
+ menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
+ if (mSplitView != null) {
+ final ViewGroup oldParent = (ViewGroup) menuView.getParent();
+ if (oldParent != null && oldParent != mSplitView) {
+ oldParent.removeView(menuView);
+ }
+ menuView.setVisibility(getAnimatedVisibility());
+ mSplitView.addView(menuView, layoutParams);
+ } else {
+ // We'll add this later if we missed it this time.
+ menuView.setLayoutParams(layoutParams);
+ }
+ }
+ mMenuView = menuView;
+ }
+
+ private void configPresenters(MenuBuilder builder) {
+ if (builder != null) {
+ builder.addMenuPresenter(mActionMenuPresenter, mPopupContext);
+ builder.addMenuPresenter(mExpandedMenuPresenter, mPopupContext);
+ } else {
+ mActionMenuPresenter.initForMenu(mPopupContext, null);
+ mExpandedMenuPresenter.initForMenu(mPopupContext, null);
+ mActionMenuPresenter.updateMenuView(true);
+ mExpandedMenuPresenter.updateMenuView(true);
+ }
+ }
+
+ public boolean hasExpandedActionView() {
+ return mExpandedMenuPresenter != null &&
+ mExpandedMenuPresenter.mCurrentExpandedItem != null;
+ }
+
+ public void collapseActionView() {
+ final MenuItemImpl item = mExpandedMenuPresenter == null ? null :
+ mExpandedMenuPresenter.mCurrentExpandedItem;
+ if (item != null) {
+ item.collapseActionView();
+ }
+ }
+
+ public void setCustomView(View view) {
+ final boolean showCustom = (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0;
+ if (mCustomNavView != null && showCustom) {
+ removeView(mCustomNavView);
+ }
+ mCustomNavView = view;
+ if (mCustomNavView != null && showCustom) {
+ addView(mCustomNavView);
+ }
+ }
+
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Set the action bar title. This will always replace or override window titles.
+ * @param title Title to set
+ *
+ * @see #setWindowTitle(CharSequence)
+ */
+ public void setTitle(CharSequence title) {
+ mUserTitle = true;
+ setTitleImpl(title);
+ }
+
+ /**
+ * Set the window title. A window title will always be replaced or overridden by a user title.
+ * @param title Title to set
+ *
+ * @see #setTitle(CharSequence)
+ */
+ public void setWindowTitle(CharSequence title) {
+ if (!mUserTitle) {
+ setTitleImpl(title);
+ }
+ }
+
+ private void setTitleImpl(CharSequence title) {
+ mTitle = title;
+ if (mTitleView != null) {
+ mTitleView.setText(title);
+ final boolean visible = mExpandedActionView == null &&
+ (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0 &&
+ (!TextUtils.isEmpty(mTitle) || !TextUtils.isEmpty(mSubtitle));
+ mTitleLayout.setVisibility(visible ? VISIBLE : GONE);
+ }
+ if (mLogoNavItem != null) {
+ mLogoNavItem.setTitle(title);
+ }
+ updateHomeAccessibility(mUpGoerFive.isEnabled());
+ }
+
+ public CharSequence getSubtitle() {
+ return mSubtitle;
+ }
+
+ public void setSubtitle(CharSequence subtitle) {
+ mSubtitle = subtitle;
+ if (mSubtitleView != null) {
+ mSubtitleView.setText(subtitle);
+ mSubtitleView.setVisibility(subtitle != null ? VISIBLE : GONE);
+ final boolean visible = mExpandedActionView == null &&
+ (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0 &&
+ (!TextUtils.isEmpty(mTitle) || !TextUtils.isEmpty(mSubtitle));
+ mTitleLayout.setVisibility(visible ? VISIBLE : GONE);
+ }
+ updateHomeAccessibility(mUpGoerFive.isEnabled());
+ }
+
+ public void setHomeButtonEnabled(boolean enable) {
+ setHomeButtonEnabled(enable, true);
+ }
+
+ private void setHomeButtonEnabled(boolean enable, boolean recordState) {
+ if (recordState) {
+ mWasHomeEnabled = enable;
+ }
+
+ if (mExpandedActionView != null) {
+ // There's an action view currently showing and we want to keep the state
+ // configured for the action view at the moment. If we needed to record the
+ // new state for later we will have done so above.
+ return;
+ }
+
+ mUpGoerFive.setEnabled(enable);
+ mUpGoerFive.setFocusable(enable);
+ // Make sure the home button has an accurate content description for accessibility.
+ updateHomeAccessibility(enable);
+ }
+
+ private void updateHomeAccessibility(boolean homeEnabled) {
+ if (!homeEnabled) {
+ mUpGoerFive.setContentDescription(null);
+ mUpGoerFive.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ } else {
+ mUpGoerFive.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ mUpGoerFive.setContentDescription(buildHomeContentDescription());
+ }
+ }
+
+ /**
+ * Compose a content description for the Home/Up affordance.
+ *
+ * <p>As this encompasses the icon/logo, title and subtitle all in one, we need
+ * a description for the whole wad of stuff that can be localized properly.</p>
+ */
+ private CharSequence buildHomeContentDescription() {
+ final CharSequence homeDesc;
+ if (mHomeDescription != null) {
+ homeDesc = mHomeDescription;
+ } else {
+ if ((mDisplayOptions & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+ homeDesc = mContext.getResources().getText(mDefaultUpDescription);
+ } else {
+ homeDesc = mContext.getResources().getText(R.string.action_bar_home_description);
+ }
+ }
+
+ final CharSequence title = getTitle();
+ final CharSequence subtitle = getSubtitle();
+ if (!TextUtils.isEmpty(title)) {
+ final String result;
+ if (!TextUtils.isEmpty(subtitle)) {
+ result = getResources().getString(
+ R.string.action_bar_home_subtitle_description_format,
+ title, subtitle, homeDesc);
+ } else {
+ result = getResources().getString(R.string.action_bar_home_description_format,
+ title, homeDesc);
+ }
+ return result;
+ }
+ return homeDesc;
+ }
+
+ public void setDisplayOptions(int options) {
+ final int flagsChanged = mDisplayOptions == -1 ? -1 : options ^ mDisplayOptions;
+ mDisplayOptions = options;
+
+ if ((flagsChanged & DISPLAY_RELAYOUT_MASK) != 0) {
+
+ if ((flagsChanged & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+ final boolean setUp = (options & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+ mHomeLayout.setShowUp(setUp);
+
+ // Showing home as up implicitly enables interaction with it.
+ // In honeycomb it was always enabled, so make this transition
+ // a bit easier for developers in the common case.
+ // (It would be silly to show it as up without responding to it.)
+ if (setUp) {
+ setHomeButtonEnabled(true);
+ }
+ }
+
+ if ((flagsChanged & ActionBar.DISPLAY_USE_LOGO) != 0) {
+ final boolean logoVis = mLogo != null && (options & ActionBar.DISPLAY_USE_LOGO) != 0;
+ mHomeLayout.setIcon(logoVis ? mLogo : mIcon);
+ }
+
+ if ((flagsChanged & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ if ((options & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ initTitle();
+ } else {
+ mUpGoerFive.removeView(mTitleLayout);
+ }
+ }
+
+ final boolean showHome = (options & ActionBar.DISPLAY_SHOW_HOME) != 0;
+ final boolean homeAsUp = (mDisplayOptions & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+ final boolean titleUp = !showHome && homeAsUp;
+ mHomeLayout.setShowIcon(showHome);
+
+ final int homeVis = (showHome || titleUp) && mExpandedActionView == null ?
+ VISIBLE : GONE;
+ mHomeLayout.setVisibility(homeVis);
+
+ if ((flagsChanged & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && mCustomNavView != null) {
+ if ((options & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+ addView(mCustomNavView);
+ } else {
+ removeView(mCustomNavView);
+ }
+ }
+
+ if (mTitleLayout != null &&
+ (flagsChanged & ActionBar.DISPLAY_TITLE_MULTIPLE_LINES) != 0) {
+ if ((options & ActionBar.DISPLAY_TITLE_MULTIPLE_LINES) != 0) {
+ mTitleView.setSingleLine(false);
+ mTitleView.setMaxLines(2);
+ } else {
+ mTitleView.setMaxLines(1);
+ mTitleView.setSingleLine(true);
+ }
+ }
+
+ requestLayout();
+ } else {
+ invalidate();
+ }
+
+ // Make sure the home button has an accurate content description for accessibility.
+ updateHomeAccessibility(mUpGoerFive.isEnabled());
+ }
+
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ if (icon != null &&
+ ((mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) == 0 || mLogo == null)) {
+ mHomeLayout.setIcon(icon);
+ }
+ if (mExpandedActionView != null) {
+ mExpandedHomeLayout.setIcon(mIcon.getConstantState().newDrawable(getResources()));
+ }
+ }
+
+ public void setIcon(int resId) {
+ setIcon(resId != 0 ? mContext.getDrawable(resId) : null);
+ }
+
+ public boolean hasIcon() {
+ return mIcon != null;
+ }
+
+ public void setLogo(Drawable logo) {
+ mLogo = logo;
+ if (logo != null && (mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) != 0) {
+ mHomeLayout.setIcon(logo);
+ }
+ }
+
+ public void setLogo(int resId) {
+ setLogo(resId != 0 ? mContext.getDrawable(resId) : null);
+ }
+
+ public boolean hasLogo() {
+ return mLogo != null;
+ }
+
+ public void setNavigationMode(int mode) {
+ final int oldMode = mNavigationMode;
+ if (mode != oldMode) {
+ switch (oldMode) {
+ case ActionBar.NAVIGATION_MODE_LIST:
+ if (mListNavLayout != null) {
+ removeView(mListNavLayout);
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabScrollView != null && mIncludeTabs) {
+ removeView(mTabScrollView);
+ }
+ }
+
+ switch (mode) {
+ case ActionBar.NAVIGATION_MODE_LIST:
+ if (mSpinner == null) {
+ mSpinner = new Spinner(mContext, null,
+ com.android.internal.R.attr.actionDropDownStyle);
+ mSpinner.setId(com.android.internal.R.id.action_bar_spinner);
+ mListNavLayout = new LinearLayout(mContext, null,
+ com.android.internal.R.attr.actionBarTabBarStyle);
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ params.gravity = Gravity.CENTER;
+ mListNavLayout.addView(mSpinner, params);
+ }
+ if (mSpinner.getAdapter() != mSpinnerAdapter) {
+ mSpinner.setAdapter(mSpinnerAdapter);
+ }
+ mSpinner.setOnItemSelectedListener(mNavItemSelectedListener);
+ addView(mListNavLayout);
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabScrollView != null && mIncludeTabs) {
+ addView(mTabScrollView);
+ }
+ break;
+ }
+ mNavigationMode = mode;
+ requestLayout();
+ }
+ }
+
+ public void setDropdownParams(SpinnerAdapter adapter, AdapterView.OnItemSelectedListener l) {
+ mSpinnerAdapter = adapter;
+ mNavItemSelectedListener = l;
+ if (mSpinner != null) {
+ mSpinner.setAdapter(adapter);
+ mSpinner.setOnItemSelectedListener(l);
+ }
+ }
+
+ public int getDropdownItemCount() {
+ return mSpinnerAdapter != null ? mSpinnerAdapter.getCount() : 0;
+ }
+
+ public void setDropdownSelectedPosition(int position) {
+ mSpinner.setSelection(position);
+ }
+
+ public int getDropdownSelectedPosition() {
+ return mSpinner.getSelectedItemPosition();
+ }
+
+ public View getCustomView() {
+ return mCustomNavView;
+ }
+
+ public int getNavigationMode() {
+ return mNavigationMode;
+ }
+
+ public int getDisplayOptions() {
+ return mDisplayOptions;
+ }
+
+ @Override
+ public ViewGroup getViewGroup() {
+ return this;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ // Used by custom nav views if they don't supply layout params. Everything else
+ // added to an ActionBarView should have them already.
+ return new ActionBar.LayoutParams(DEFAULT_CUSTOM_GRAVITY);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mUpGoerFive.addView(mHomeLayout, 0);
+ addView(mUpGoerFive);
+
+ if (mCustomNavView != null && (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+ final ViewParent parent = mCustomNavView.getParent();
+ if (parent != this) {
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(mCustomNavView);
+ }
+ addView(mCustomNavView);
+ }
+ }
+ }
+
+ private void initTitle() {
+ if (mTitleLayout == null) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ mTitleLayout = (LinearLayout) inflater.inflate(R.layout.action_bar_title_item,
+ this, false);
+ mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title);
+ mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle);
+
+ if (mTitleStyleRes != 0) {
+ mTitleView.setTextAppearance(mTitleStyleRes);
+ }
+ if (mTitle != null) {
+ mTitleView.setText(mTitle);
+ }
+
+ if (mSubtitleStyleRes != 0) {
+ mSubtitleView.setTextAppearance(mSubtitleStyleRes);
+ }
+ if (mSubtitle != null) {
+ mSubtitleView.setText(mSubtitle);
+ mSubtitleView.setVisibility(VISIBLE);
+ }
+ }
+
+ mUpGoerFive.addView(mTitleLayout);
+ if (mExpandedActionView != null ||
+ (TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mSubtitle))) {
+ // Don't show while in expanded mode or with empty text
+ mTitleLayout.setVisibility(GONE);
+ } else {
+ mTitleLayout.setVisibility(VISIBLE);
+ }
+ }
+
+ public void setContextView(ActionBarContextView view) {
+ mContextView = view;
+ }
+
+ public void setCollapsible(boolean collapsible) {
+ mIsCollapsible = collapsible;
+ }
+
+ /**
+ * @return True if any characters in the title were truncated
+ */
+ public boolean isTitleTruncated() {
+ if (mTitleView == null) {
+ return false;
+ }
+
+ final Layout titleLayout = mTitleView.getLayout();
+ if (titleLayout == null) {
+ return false;
+ }
+
+ final int lineCount = titleLayout.getLineCount();
+ for (int i = 0; i < lineCount; i++) {
+ if (titleLayout.getEllipsisCount(i) > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int childCount = getChildCount();
+ if (mIsCollapsible) {
+ int visibleChildren = 0;
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE &&
+ !(child == mMenuView && mMenuView.getChildCount() == 0) &&
+ child != mUpGoerFive) {
+ visibleChildren++;
+ }
+ }
+
+ final int upChildCount = mUpGoerFive.getChildCount();
+ for (int i = 0; i < upChildCount; i++) {
+ final View child = mUpGoerFive.getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ visibleChildren++;
+ }
+ }
+
+ if (visibleChildren == 0) {
+ // No size for an empty action bar when collapsable.
+ setMeasuredDimension(0, 0);
+ return;
+ }
+ }
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_width=\"match_parent\" (or fill_parent)");
+ }
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode != MeasureSpec.AT_MOST) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_height=\"wrap_content\"");
+ }
+
+ int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
+
+ int maxHeight = mContentHeight >= 0 ?
+ mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
+
+ final int verticalPadding = getPaddingTop() + getPaddingBottom();
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int height = maxHeight - verticalPadding;
+ final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+ final int exactHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+ int availableWidth = contentWidth - paddingLeft - paddingRight;
+ int leftOfCenter = availableWidth / 2;
+ int rightOfCenter = leftOfCenter;
+
+ final boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE &&
+ (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0;
+
+ HomeView homeLayout = mExpandedActionView != null ? mExpandedHomeLayout : mHomeLayout;
+
+ final ViewGroup.LayoutParams homeLp = homeLayout.getLayoutParams();
+ int homeWidthSpec;
+ if (homeLp.width < 0) {
+ homeWidthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST);
+ } else {
+ homeWidthSpec = MeasureSpec.makeMeasureSpec(homeLp.width, MeasureSpec.EXACTLY);
+ }
+
+ /*
+ * This is a little weird.
+ * We're only measuring the *home* affordance within the Up container here
+ * on purpose, because we want to give the available space to all other views before
+ * the title text. We'll remeasure the whole up container again later.
+ * We need to measure this container so we know the right offset for the up affordance
+ * no matter what.
+ */
+ homeLayout.measure(homeWidthSpec, exactHeightSpec);
+
+ int homeWidth = 0;
+ if ((homeLayout.getVisibility() != GONE && homeLayout.getParent() == mUpGoerFive)
+ || showTitle) {
+ homeWidth = homeLayout.getMeasuredWidth();
+ final int homeOffsetWidth = homeWidth + homeLayout.getStartOffset();
+ availableWidth = Math.max(0, availableWidth - homeOffsetWidth);
+ leftOfCenter = Math.max(0, availableWidth - homeOffsetWidth);
+ }
+
+ if (mMenuView != null && mMenuView.getParent() == this) {
+ availableWidth = measureChildView(mMenuView, availableWidth, exactHeightSpec, 0);
+ rightOfCenter = Math.max(0, rightOfCenter - mMenuView.getMeasuredWidth());
+ }
+
+ if (mIndeterminateProgressView != null &&
+ mIndeterminateProgressView.getVisibility() != GONE) {
+ availableWidth = measureChildView(mIndeterminateProgressView, availableWidth,
+ childSpecHeight, 0);
+ rightOfCenter = Math.max(0,
+ rightOfCenter - mIndeterminateProgressView.getMeasuredWidth());
+ }
+
+ if (mExpandedActionView == null) {
+ switch (mNavigationMode) {
+ case ActionBar.NAVIGATION_MODE_LIST:
+ if (mListNavLayout != null) {
+ final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding;
+ availableWidth = Math.max(0, availableWidth - itemPaddingSize);
+ leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize);
+ mListNavLayout.measure(
+ MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ final int listNavWidth = mListNavLayout.getMeasuredWidth();
+ availableWidth = Math.max(0, availableWidth - listNavWidth);
+ leftOfCenter = Math.max(0, leftOfCenter - listNavWidth);
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabScrollView != null) {
+ final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding;
+ availableWidth = Math.max(0, availableWidth - itemPaddingSize);
+ leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize);
+ mTabScrollView.measure(
+ MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ final int tabWidth = mTabScrollView.getMeasuredWidth();
+ availableWidth = Math.max(0, availableWidth - tabWidth);
+ leftOfCenter = Math.max(0, leftOfCenter - tabWidth);
+ }
+ break;
+ }
+ }
+
+ View customView = null;
+ if (mExpandedActionView != null) {
+ customView = mExpandedActionView;
+ } else if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 &&
+ mCustomNavView != null) {
+ customView = mCustomNavView;
+ }
+
+ if (customView != null) {
+ final ViewGroup.LayoutParams lp = generateLayoutParams(customView.getLayoutParams());
+ final ActionBar.LayoutParams ablp = lp instanceof ActionBar.LayoutParams ?
+ (ActionBar.LayoutParams) lp : null;
+
+ int horizontalMargin = 0;
+ int verticalMargin = 0;
+ if (ablp != null) {
+ horizontalMargin = ablp.leftMargin + ablp.rightMargin;
+ verticalMargin = ablp.topMargin + ablp.bottomMargin;
+ }
+
+ // If the action bar is wrapping to its content height, don't allow a custom
+ // view to MATCH_PARENT.
+ int customNavHeightMode;
+ if (mContentHeight <= 0) {
+ customNavHeightMode = MeasureSpec.AT_MOST;
+ } else {
+ customNavHeightMode = lp.height != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ }
+ final int customNavHeight = Math.max(0,
+ (lp.height >= 0 ? Math.min(lp.height, height) : height) - verticalMargin);
+
+ final int customNavWidthMode = lp.width != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ int customNavWidth = Math.max(0,
+ (lp.width >= 0 ? Math.min(lp.width, availableWidth) : availableWidth)
+ - horizontalMargin);
+ final int hgrav = (ablp != null ? ablp.gravity : DEFAULT_CUSTOM_GRAVITY) &
+ Gravity.HORIZONTAL_GRAVITY_MASK;
+
+ // Centering a custom view is treated specially; we try to center within the whole
+ // action bar rather than in the available space.
+ if (hgrav == Gravity.CENTER_HORIZONTAL && lp.width == LayoutParams.MATCH_PARENT) {
+ customNavWidth = Math.min(leftOfCenter, rightOfCenter) * 2;
+ }
+
+ customView.measure(
+ MeasureSpec.makeMeasureSpec(customNavWidth, customNavWidthMode),
+ MeasureSpec.makeMeasureSpec(customNavHeight, customNavHeightMode));
+ availableWidth -= horizontalMargin + customView.getMeasuredWidth();
+ }
+
+ /*
+ * Measure the whole up container now, allowing for the full home+title sections.
+ * (This will re-measure the home view.)
+ */
+ availableWidth = measureChildView(mUpGoerFive, availableWidth + homeWidth,
+ MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY), 0);
+ if (mTitleLayout != null) {
+ leftOfCenter = Math.max(0, leftOfCenter - mTitleLayout.getMeasuredWidth());
+ }
+
+ if (mContentHeight <= 0) {
+ int measuredHeight = 0;
+ for (int i = 0; i < childCount; i++) {
+ View v = getChildAt(i);
+ int paddedViewHeight = v.getMeasuredHeight() + verticalPadding;
+ if (paddedViewHeight > measuredHeight) {
+ measuredHeight = paddedViewHeight;
+ }
+ }
+ setMeasuredDimension(contentWidth, measuredHeight);
+ } else {
+ setMeasuredDimension(contentWidth, maxHeight);
+ }
+
+ if (mContextView != null) {
+ mContextView.setContentHeight(getMeasuredHeight());
+ }
+
+ if (mProgressView != null && mProgressView.getVisibility() != GONE) {
+ mProgressView.measure(MeasureSpec.makeMeasureSpec(
+ contentWidth - mProgressBarPadding * 2, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int contentHeight = b - t - getPaddingTop() - getPaddingBottom();
+
+ if (contentHeight <= 0) {
+ // Nothing to do if we can't see anything.
+ return;
+ }
+
+ final boolean isLayoutRtl = isLayoutRtl();
+ final int direction = isLayoutRtl ? 1 : -1;
+ int menuStart = isLayoutRtl ? getPaddingLeft() : r - l - getPaddingRight();
+ // In LTR mode, we start from left padding and go to the right; in RTL mode, we start
+ // from the padding right and go to the left (in reverse way)
+ int x = isLayoutRtl ? r - l - getPaddingRight() : getPaddingLeft();
+ final int y = getPaddingTop();
+
+ HomeView homeLayout = mExpandedActionView != null ? mExpandedHomeLayout : mHomeLayout;
+ final boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE &&
+ (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0;
+ int startOffset = 0;
+ if (homeLayout.getParent() == mUpGoerFive) {
+ if (homeLayout.getVisibility() != GONE) {
+ startOffset = homeLayout.getStartOffset();
+ } else if (showTitle) {
+ startOffset = homeLayout.getUpWidth();
+ }
+ }
+
+ // Position the up container based on where the edge of the home layout should go.
+ x += positionChild(mUpGoerFive,
+ next(x, startOffset, isLayoutRtl), y, contentHeight, isLayoutRtl);
+ x = next(x, startOffset, isLayoutRtl);
+
+ if (mExpandedActionView == null) {
+ switch (mNavigationMode) {
+ case ActionBar.NAVIGATION_MODE_STANDARD:
+ break;
+ case ActionBar.NAVIGATION_MODE_LIST:
+ if (mListNavLayout != null) {
+ if (showTitle) {
+ x = next(x, mItemPadding, isLayoutRtl);
+ }
+ x += positionChild(mListNavLayout, x, y, contentHeight, isLayoutRtl);
+ x = next(x, mItemPadding, isLayoutRtl);
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabScrollView != null) {
+ if (showTitle) x = next(x, mItemPadding, isLayoutRtl);
+ x += positionChild(mTabScrollView, x, y, contentHeight, isLayoutRtl);
+ x = next(x, mItemPadding, isLayoutRtl);
+ }
+ break;
+ }
+ }
+
+ if (mMenuView != null && mMenuView.getParent() == this) {
+ positionChild(mMenuView, menuStart, y, contentHeight, !isLayoutRtl);
+ menuStart += direction * mMenuView.getMeasuredWidth();
+ }
+
+ if (mIndeterminateProgressView != null &&
+ mIndeterminateProgressView.getVisibility() != GONE) {
+ positionChild(mIndeterminateProgressView, menuStart, y, contentHeight, !isLayoutRtl);
+ menuStart += direction * mIndeterminateProgressView.getMeasuredWidth();
+ }
+
+ View customView = null;
+ if (mExpandedActionView != null) {
+ customView = mExpandedActionView;
+ } else if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 &&
+ mCustomNavView != null) {
+ customView = mCustomNavView;
+ }
+ if (customView != null) {
+ final int layoutDirection = getLayoutDirection();
+ ViewGroup.LayoutParams lp = customView.getLayoutParams();
+ final ActionBar.LayoutParams ablp = lp instanceof ActionBar.LayoutParams ?
+ (ActionBar.LayoutParams) lp : null;
+ final int gravity = ablp != null ? ablp.gravity : DEFAULT_CUSTOM_GRAVITY;
+ final int navWidth = customView.getMeasuredWidth();
+
+ int topMargin = 0;
+ int bottomMargin = 0;
+ if (ablp != null) {
+ x = next(x, ablp.getMarginStart(), isLayoutRtl);
+ menuStart += direction * ablp.getMarginEnd();
+ topMargin = ablp.topMargin;
+ bottomMargin = ablp.bottomMargin;
+ }
+
+ int hgravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+ // See if we actually have room to truly center; if not push against left or right.
+ if (hgravity == Gravity.CENTER_HORIZONTAL) {
+ final int centeredLeft = ((mRight - mLeft) - navWidth) / 2;
+ if (isLayoutRtl) {
+ final int centeredStart = centeredLeft + navWidth;
+ final int centeredEnd = centeredLeft;
+ if (centeredStart > x) {
+ hgravity = Gravity.RIGHT;
+ } else if (centeredEnd < menuStart) {
+ hgravity = Gravity.LEFT;
+ }
+ } else {
+ final int centeredStart = centeredLeft;
+ final int centeredEnd = centeredLeft + navWidth;
+ if (centeredStart < x) {
+ hgravity = Gravity.LEFT;
+ } else if (centeredEnd > menuStart) {
+ hgravity = Gravity.RIGHT;
+ }
+ }
+ } else if (gravity == Gravity.NO_GRAVITY) {
+ hgravity = Gravity.START;
+ }
+
+ int xpos = 0;
+ switch (Gravity.getAbsoluteGravity(hgravity, layoutDirection)) {
+ case Gravity.CENTER_HORIZONTAL:
+ xpos = ((mRight - mLeft) - navWidth) / 2;
+ break;
+ case Gravity.LEFT:
+ xpos = isLayoutRtl ? menuStart : x;
+ break;
+ case Gravity.RIGHT:
+ xpos = isLayoutRtl ? x - navWidth : menuStart - navWidth;
+ break;
+ }
+
+ int vgravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ if (gravity == Gravity.NO_GRAVITY) {
+ vgravity = Gravity.CENTER_VERTICAL;
+ }
+
+ int ypos = 0;
+ switch (vgravity) {
+ case Gravity.CENTER_VERTICAL:
+ final int paddedTop = getPaddingTop();
+ final int paddedBottom = mBottom - mTop - getPaddingBottom();
+ ypos = ((paddedBottom - paddedTop) - customView.getMeasuredHeight()) / 2;
+ break;
+ case Gravity.TOP:
+ ypos = getPaddingTop() + topMargin;
+ break;
+ case Gravity.BOTTOM:
+ ypos = getHeight() - getPaddingBottom() - customView.getMeasuredHeight()
+ - bottomMargin;
+ break;
+ }
+ final int customWidth = customView.getMeasuredWidth();
+ customView.layout(xpos, ypos, xpos + customWidth,
+ ypos + customView.getMeasuredHeight());
+ x = next(x, customWidth, isLayoutRtl);
+ }
+
+ if (mProgressView != null) {
+ mProgressView.bringToFront();
+ final int halfProgressHeight = mProgressView.getMeasuredHeight() / 2;
+ mProgressView.layout(mProgressBarPadding, -halfProgressHeight,
+ mProgressBarPadding + mProgressView.getMeasuredWidth(), halfProgressHeight);
+ }
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new ActionBar.LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp == null) {
+ lp = generateDefaultLayoutParams();
+ }
+ return lp;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState state = new SavedState(superState);
+
+ if (mExpandedMenuPresenter != null && mExpandedMenuPresenter.mCurrentExpandedItem != null) {
+ state.expandedMenuItemId = mExpandedMenuPresenter.mCurrentExpandedItem.getItemId();
+ }
+
+ state.isOverflowOpen = isOverflowMenuShowing();
+
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable p) {
+ SavedState state = (SavedState) p;
+
+ super.onRestoreInstanceState(state.getSuperState());
+
+ if (state.expandedMenuItemId != 0 &&
+ mExpandedMenuPresenter != null && mOptionsMenu != null) {
+ final MenuItem item = mOptionsMenu.findItem(state.expandedMenuItemId);
+ if (item != null) {
+ item.expandActionView();
+ }
+ }
+
+ if (state.isOverflowOpen) {
+ postShowOverflowMenu();
+ }
+ }
+
+ public void setNavigationIcon(Drawable indicator) {
+ mHomeLayout.setUpIndicator(indicator);
+ }
+
+ @Override
+ public void setDefaultNavigationIcon(Drawable icon) {
+ mHomeLayout.setDefaultUpIndicator(icon);
+ }
+
+ public void setNavigationIcon(int resId) {
+ mHomeLayout.setUpIndicator(resId);
+ }
+
+ public void setNavigationContentDescription(CharSequence description) {
+ mHomeDescription = description;
+ updateHomeAccessibility(mUpGoerFive.isEnabled());
+ }
+
+ public void setNavigationContentDescription(int resId) {
+ mHomeDescriptionRes = resId;
+ mHomeDescription = resId != 0 ? getResources().getText(resId) : null;
+ updateHomeAccessibility(mUpGoerFive.isEnabled());
+ }
+
+ @Override
+ public void setDefaultNavigationContentDescription(int defaultNavigationContentDescription) {
+ if (mDefaultUpDescription == defaultNavigationContentDescription) {
+ return;
+ }
+ mDefaultUpDescription = defaultNavigationContentDescription;
+ updateHomeAccessibility(mUpGoerFive.isEnabled());
+ }
+
+ @Override
+ public void setMenuCallbacks(MenuPresenter.Callback presenterCallback,
+ MenuBuilder.Callback menuBuilderCallback) {
+ if (mActionMenuPresenter != null) {
+ mActionMenuPresenter.setCallback(presenterCallback);
+ }
+ if (mOptionsMenu != null) {
+ mOptionsMenu.setCallback(menuBuilderCallback);
+ }
+ }
+
+ @Override
+ public Menu getMenu() {
+ return mOptionsMenu;
+ }
+
+ static class SavedState extends BaseSavedState {
+ int expandedMenuItemId;
+ boolean isOverflowOpen;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ expandedMenuItemId = in.readInt();
+ isOverflowOpen = in.readInt() != 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(expandedMenuItemId);
+ out.writeInt(isOverflowOpen ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ private static class HomeView extends FrameLayout {
+ private ImageView mUpView;
+ private ImageView mIconView;
+ private int mUpWidth;
+ private int mStartOffset;
+ private int mUpIndicatorRes;
+ private Drawable mDefaultUpIndicator;
+ private Drawable mUpIndicator;
+
+ private static final long DEFAULT_TRANSITION_DURATION = 150;
+
+ public HomeView(Context context) {
+ this(context, null);
+ }
+
+ public HomeView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ LayoutTransition t = getLayoutTransition();
+ if (t != null) {
+ // Set a lower duration than the default
+ t.setDuration(DEFAULT_TRANSITION_DURATION);
+ }
+ }
+
+ public void setShowUp(boolean isUp) {
+ mUpView.setVisibility(isUp ? VISIBLE : GONE);
+ }
+
+ public void setShowIcon(boolean showIcon) {
+ mIconView.setVisibility(showIcon ? VISIBLE : GONE);
+ }
+
+ public void setIcon(Drawable icon) {
+ mIconView.setImageDrawable(icon);
+ }
+
+ public void setUpIndicator(Drawable d) {
+ mUpIndicator = d;
+ mUpIndicatorRes = 0;
+ updateUpIndicator();
+ }
+
+ public void setDefaultUpIndicator(Drawable d) {
+ mDefaultUpIndicator = d;
+ updateUpIndicator();
+ }
+
+ public void setUpIndicator(int resId) {
+ mUpIndicatorRes = resId;
+ mUpIndicator = null;
+ updateUpIndicator();
+ }
+
+ private void updateUpIndicator() {
+ if (mUpIndicator != null) {
+ mUpView.setImageDrawable(mUpIndicator);
+ } else if (mUpIndicatorRes != 0) {
+ mUpView.setImageDrawable(getContext().getDrawable(mUpIndicatorRes));
+ } else {
+ mUpView.setImageDrawable(mDefaultUpIndicator);
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mUpIndicatorRes != 0) {
+ // Reload for config change
+ updateUpIndicator();
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ onPopulateAccessibilityEvent(event);
+ return true;
+ }
+
+ @Override
+ public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEventInternal(event);
+ final CharSequence cdesc = getContentDescription();
+ if (!TextUtils.isEmpty(cdesc)) {
+ event.getText().add(cdesc);
+ }
+ }
+
+ @Override
+ public boolean dispatchHoverEvent(MotionEvent event) {
+ // Don't allow children to hover; we want this to be treated as a single component.
+ return onHoverEvent(event);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mUpView = (ImageView) findViewById(com.android.internal.R.id.up);
+ mIconView = (ImageView) findViewById(com.android.internal.R.id.home);
+ mDefaultUpIndicator = mUpView.getDrawable();
+ }
+
+ public int getStartOffset() {
+ return mUpView.getVisibility() == GONE ? mStartOffset : 0;
+ }
+
+ public int getUpWidth() {
+ return mUpWidth;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ measureChildWithMargins(mUpView, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ final LayoutParams upLp = (LayoutParams) mUpView.getLayoutParams();
+ final int upMargins = upLp.leftMargin + upLp.rightMargin;
+ mUpWidth = mUpView.getMeasuredWidth();
+ mStartOffset = mUpWidth + upMargins;
+ int width = mUpView.getVisibility() == GONE ? 0 : mStartOffset;
+ int height = upLp.topMargin + mUpView.getMeasuredHeight() + upLp.bottomMargin;
+
+ if (mIconView.getVisibility() != GONE) {
+ measureChildWithMargins(mIconView, widthMeasureSpec, width, heightMeasureSpec, 0);
+ final LayoutParams iconLp = (LayoutParams) mIconView.getLayoutParams();
+ width += iconLp.leftMargin + mIconView.getMeasuredWidth() + iconLp.rightMargin;
+ height = Math.max(height,
+ iconLp.topMargin + mIconView.getMeasuredHeight() + iconLp.bottomMargin);
+ } else if (upMargins < 0) {
+ // Remove the measurement effects of negative margins used for offsets
+ width -= upMargins;
+ }
+
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ switch (widthMode) {
+ case MeasureSpec.AT_MOST:
+ width = Math.min(width, widthSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ width = widthSize;
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ break;
+ }
+ switch (heightMode) {
+ case MeasureSpec.AT_MOST:
+ height = Math.min(height, heightSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ height = heightSize;
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ break;
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int vCenter = (b - t) / 2;
+ final boolean isLayoutRtl = isLayoutRtl();
+ final int width = getWidth();
+ int upOffset = 0;
+ if (mUpView.getVisibility() != GONE) {
+ final LayoutParams upLp = (LayoutParams) mUpView.getLayoutParams();
+ final int upHeight = mUpView.getMeasuredHeight();
+ final int upWidth = mUpView.getMeasuredWidth();
+ upOffset = upLp.leftMargin + upWidth + upLp.rightMargin;
+ final int upTop = vCenter - upHeight / 2;
+ final int upBottom = upTop + upHeight;
+ final int upRight;
+ final int upLeft;
+ if (isLayoutRtl) {
+ upRight = width;
+ upLeft = upRight - upWidth;
+ r -= upOffset;
+ } else {
+ upRight = upWidth;
+ upLeft = 0;
+ l += upOffset;
+ }
+ mUpView.layout(upLeft, upTop, upRight, upBottom);
+ }
+
+ final LayoutParams iconLp = (LayoutParams) mIconView.getLayoutParams();
+ final int iconHeight = mIconView.getMeasuredHeight();
+ final int iconWidth = mIconView.getMeasuredWidth();
+ final int hCenter = (r - l) / 2;
+ final int iconTop = Math.max(iconLp.topMargin, vCenter - iconHeight / 2);
+ final int iconBottom = iconTop + iconHeight;
+ final int iconLeft;
+ final int iconRight;
+ int marginStart = iconLp.getMarginStart();
+ final int delta = Math.max(marginStart, hCenter - iconWidth / 2);
+ if (isLayoutRtl) {
+ iconRight = width - upOffset - delta;
+ iconLeft = iconRight - iconWidth;
+ } else {
+ iconLeft = upOffset + delta;
+ iconRight = iconLeft + iconWidth;
+ }
+ mIconView.layout(iconLeft, iconTop, iconRight, iconBottom);
+ }
+ }
+
+ private class ExpandedActionViewMenuPresenter implements MenuPresenter {
+ MenuBuilder mMenu;
+ MenuItemImpl mCurrentExpandedItem;
+
+ @Override
+ public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+ // Clear the expanded action view when menus change.
+ if (mMenu != null && mCurrentExpandedItem != null) {
+ mMenu.collapseItemActionView(mCurrentExpandedItem);
+ }
+ mMenu = menu;
+ }
+
+ @Override
+ public MenuView getMenuView(ViewGroup root) {
+ return null;
+ }
+
+ @Override
+ public void updateMenuView(boolean cleared) {
+ // Make sure the expanded item we have is still there.
+ if (mCurrentExpandedItem != null) {
+ boolean found = false;
+
+ if (mMenu != null) {
+ final int count = mMenu.size();
+ for (int i = 0; i < count; i++) {
+ final MenuItem item = mMenu.getItem(i);
+ if (item == mCurrentExpandedItem) {
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found) {
+ // The item we had expanded disappeared. Collapse.
+ collapseItemActionView(mMenu, mCurrentExpandedItem);
+ }
+ }
+ }
+
+ @Override
+ public void setCallback(Callback cb) {
+ }
+
+ @Override
+ public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+ return false;
+ }
+
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+ }
+
+ @Override
+ public boolean flagActionItems() {
+ return false;
+ }
+
+ @Override
+ public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+
+ mExpandedActionView = item.getActionView();
+ mExpandedHomeLayout.setIcon(mIcon.getConstantState().newDrawable(getResources()));
+ mCurrentExpandedItem = item;
+ if (mExpandedActionView.getParent() != ActionBarView.this) {
+ addView(mExpandedActionView);
+ }
+ if (mExpandedHomeLayout.getParent() != mUpGoerFive) {
+ mUpGoerFive.addView(mExpandedHomeLayout);
+ }
+ mHomeLayout.setVisibility(GONE);
+ if (mTitleLayout != null) mTitleLayout.setVisibility(GONE);
+ if (mTabScrollView != null) mTabScrollView.setVisibility(GONE);
+ if (mSpinner != null) mSpinner.setVisibility(GONE);
+ if (mCustomNavView != null) mCustomNavView.setVisibility(GONE);
+ setHomeButtonEnabled(false, false);
+ requestLayout();
+ item.setActionViewExpanded(true);
+
+ if (mExpandedActionView instanceof CollapsibleActionView) {
+ ((CollapsibleActionView) mExpandedActionView).onActionViewExpanded();
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+
+ // Do this before detaching the actionview from the hierarchy, in case
+ // it needs to dismiss the soft keyboard, etc.
+ if (mExpandedActionView instanceof CollapsibleActionView) {
+ ((CollapsibleActionView) mExpandedActionView).onActionViewCollapsed();
+ }
+
+ removeView(mExpandedActionView);
+ mUpGoerFive.removeView(mExpandedHomeLayout);
+ mExpandedActionView = null;
+ if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_HOME) != 0) {
+ mHomeLayout.setVisibility(VISIBLE);
+ }
+ if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ if (mTitleLayout == null) {
+ initTitle();
+ } else {
+ mTitleLayout.setVisibility(VISIBLE);
+ }
+ }
+ if (mTabScrollView != null) mTabScrollView.setVisibility(VISIBLE);
+ if (mSpinner != null) mSpinner.setVisibility(VISIBLE);
+ if (mCustomNavView != null) mCustomNavView.setVisibility(VISIBLE);
+
+ mExpandedHomeLayout.setIcon(null);
+ mCurrentExpandedItem = null;
+ setHomeButtonEnabled(mWasHomeEnabled); // Set by expandItemActionView above
+ requestLayout();
+ item.setActionViewExpanded(false);
+
+ return true;
+ }
+
+ @Override
+ public int getId() {
+ return 0;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ return null;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ }
+ }
+}
diff --git a/com/android/internal/widget/AdapterHelper.java b/com/android/internal/widget/AdapterHelper.java
new file mode 100644
index 0000000..f47d430
--- /dev/null
+++ b/com/android/internal/widget/AdapterHelper.java
@@ -0,0 +1,775 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.util.Log;
+import android.util.Pools;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Helper class that can enqueue and process adapter update operations.
+ * <p>
+ * To support animations, RecyclerView presents an older version the Adapter to best represent
+ * previous state of the layout. Sometimes, this is not trivial when items are removed that were
+ * not laid out, in which case, RecyclerView has no way of providing that item's view for
+ * animations.
+ * <p>
+ * AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
+ * pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
+ * and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
+ * according to previously deferred operation and dispatch them before the first layout pass. It
+ * also takes care of updating deferred UpdateOps since order of operations is changed by this
+ * process.
+ * <p>
+ * Although operations may be forwarded to LayoutManager in different orders, resulting data set
+ * is guaranteed to be the consistent.
+ */
+class AdapterHelper implements OpReorderer.Callback {
+
+ static final int POSITION_TYPE_INVISIBLE = 0;
+
+ static final int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
+
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "AHT";
+
+ private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
+
+ final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
+
+ final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
+
+ final Callback mCallback;
+
+ Runnable mOnItemProcessedCallback;
+
+ final boolean mDisableRecycler;
+
+ final OpReorderer mOpReorderer;
+
+ private int mExistingUpdateTypes = 0;
+
+ AdapterHelper(Callback callback) {
+ this(callback, false);
+ }
+
+ AdapterHelper(Callback callback, boolean disableRecycler) {
+ mCallback = callback;
+ mDisableRecycler = disableRecycler;
+ mOpReorderer = new OpReorderer(this);
+ }
+
+ AdapterHelper addUpdateOp(UpdateOp... ops) {
+ Collections.addAll(mPendingUpdates, ops);
+ return this;
+ }
+
+ void reset() {
+ recycleUpdateOpsAndClearList(mPendingUpdates);
+ recycleUpdateOpsAndClearList(mPostponedList);
+ mExistingUpdateTypes = 0;
+ }
+
+ void preProcess() {
+ mOpReorderer.reorderOps(mPendingUpdates);
+ final int count = mPendingUpdates.size();
+ for (int i = 0; i < count; i++) {
+ UpdateOp op = mPendingUpdates.get(i);
+ switch (op.cmd) {
+ case UpdateOp.ADD:
+ applyAdd(op);
+ break;
+ case UpdateOp.REMOVE:
+ applyRemove(op);
+ break;
+ case UpdateOp.UPDATE:
+ applyUpdate(op);
+ break;
+ case UpdateOp.MOVE:
+ applyMove(op);
+ break;
+ }
+ if (mOnItemProcessedCallback != null) {
+ mOnItemProcessedCallback.run();
+ }
+ }
+ mPendingUpdates.clear();
+ }
+
+ void consumePostponedUpdates() {
+ final int count = mPostponedList.size();
+ for (int i = 0; i < count; i++) {
+ mCallback.onDispatchSecondPass(mPostponedList.get(i));
+ }
+ recycleUpdateOpsAndClearList(mPostponedList);
+ mExistingUpdateTypes = 0;
+ }
+
+ private void applyMove(UpdateOp op) {
+ // MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
+ // otherwise, it would be converted into a REMOVE operation
+ postponeAndUpdateViewHolders(op);
+ }
+
+ private void applyRemove(UpdateOp op) {
+ int tmpStart = op.positionStart;
+ int tmpCount = 0;
+ int tmpEnd = op.positionStart + op.itemCount;
+ int type = -1;
+ for (int position = op.positionStart; position < tmpEnd; position++) {
+ boolean typeChanged = false;
+ RecyclerView.ViewHolder vh = mCallback.findViewHolder(position);
+ if (vh != null || canFindInPreLayout(position)) {
+ // If a ViewHolder exists or this is a newly added item, we can defer this update
+ // to post layout stage.
+ // * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
+ // * For items that are added and removed in the same process cycle, they won't
+ // have any effect in pre-layout since their add ops are already deferred to
+ // post-layout pass.
+ if (type == POSITION_TYPE_INVISIBLE) {
+ // Looks like we have other updates that we cannot merge with this one.
+ // Create an UpdateOp and dispatch it to LayoutManager.
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
+ dispatchAndUpdateViewHolders(newOp);
+ typeChanged = true;
+ }
+ type = POSITION_TYPE_NEW_OR_LAID_OUT;
+ } else {
+ // This update cannot be recovered because we don't have a ViewHolder representing
+ // this position. Instead, post it to LayoutManager immediately
+ if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
+ // Looks like we have other updates that we cannot merge with this one.
+ // Create UpdateOp op and dispatch it to LayoutManager.
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
+ postponeAndUpdateViewHolders(newOp);
+ typeChanged = true;
+ }
+ type = POSITION_TYPE_INVISIBLE;
+ }
+ if (typeChanged) {
+ position -= tmpCount; // also equal to tmpStart
+ tmpEnd -= tmpCount;
+ tmpCount = 1;
+ } else {
+ tmpCount++;
+ }
+ }
+ if (tmpCount != op.itemCount) { // all 1 effect
+ recycleUpdateOp(op);
+ op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
+ }
+ if (type == POSITION_TYPE_INVISIBLE) {
+ dispatchAndUpdateViewHolders(op);
+ } else {
+ postponeAndUpdateViewHolders(op);
+ }
+ }
+
+ private void applyUpdate(UpdateOp op) {
+ int tmpStart = op.positionStart;
+ int tmpCount = 0;
+ int tmpEnd = op.positionStart + op.itemCount;
+ int type = -1;
+ for (int position = op.positionStart; position < tmpEnd; position++) {
+ RecyclerView.ViewHolder vh = mCallback.findViewHolder(position);
+ if (vh != null || canFindInPreLayout(position)) { // deferred
+ if (type == POSITION_TYPE_INVISIBLE) {
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
+ op.payload);
+ dispatchAndUpdateViewHolders(newOp);
+ tmpCount = 0;
+ tmpStart = position;
+ }
+ type = POSITION_TYPE_NEW_OR_LAID_OUT;
+ } else { // applied
+ if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
+ op.payload);
+ postponeAndUpdateViewHolders(newOp);
+ tmpCount = 0;
+ tmpStart = position;
+ }
+ type = POSITION_TYPE_INVISIBLE;
+ }
+ tmpCount++;
+ }
+ if (tmpCount != op.itemCount) { // all 1 effect
+ Object payload = op.payload;
+ recycleUpdateOp(op);
+ op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);
+ }
+ if (type == POSITION_TYPE_INVISIBLE) {
+ dispatchAndUpdateViewHolders(op);
+ } else {
+ postponeAndUpdateViewHolders(op);
+ }
+ }
+
+ private void dispatchAndUpdateViewHolders(UpdateOp op) {
+ // tricky part.
+ // traverse all postpones and revert their changes on this op if necessary, apply updated
+ // dispatch to them since now they are after this op.
+ if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
+ throw new IllegalArgumentException("should not dispatch add or move for pre layout");
+ }
+ if (DEBUG) {
+ Log.d(TAG, "dispatch (pre)" + op);
+ Log.d(TAG, "postponed state before:");
+ for (UpdateOp updateOp : mPostponedList) {
+ Log.d(TAG, updateOp.toString());
+ }
+ Log.d(TAG, "----");
+ }
+
+ // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
+ // TODO Since move ops are pushed to end, we should not need this anymore
+ int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
+ if (DEBUG) {
+ Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
+ }
+ int tmpCnt = 1;
+ int offsetPositionForPartial = op.positionStart;
+ final int positionMultiplier;
+ switch (op.cmd) {
+ case UpdateOp.UPDATE:
+ positionMultiplier = 1;
+ break;
+ case UpdateOp.REMOVE:
+ positionMultiplier = 0;
+ break;
+ default:
+ throw new IllegalArgumentException("op should be remove or update." + op);
+ }
+ for (int p = 1; p < op.itemCount; p++) {
+ final int pos = op.positionStart + (positionMultiplier * p);
+ int updatedPos = updatePositionWithPostponed(pos, op.cmd);
+ if (DEBUG) {
+ Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
+ }
+ boolean continuous = false;
+ switch (op.cmd) {
+ case UpdateOp.UPDATE:
+ continuous = updatedPos == tmpStart + 1;
+ break;
+ case UpdateOp.REMOVE:
+ continuous = updatedPos == tmpStart;
+ break;
+ }
+ if (continuous) {
+ tmpCnt++;
+ } else {
+ // need to dispatch this separately
+ UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, op.payload);
+ if (DEBUG) {
+ Log.d(TAG, "need to dispatch separately " + tmp);
+ }
+ dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
+ recycleUpdateOp(tmp);
+ if (op.cmd == UpdateOp.UPDATE) {
+ offsetPositionForPartial += tmpCnt;
+ }
+ tmpStart = updatedPos; // need to remove previously dispatched
+ tmpCnt = 1;
+ }
+ }
+ Object payload = op.payload;
+ recycleUpdateOp(op);
+ if (tmpCnt > 0) {
+ UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, payload);
+ if (DEBUG) {
+ Log.d(TAG, "dispatching:" + tmp);
+ }
+ dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
+ recycleUpdateOp(tmp);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "post dispatch");
+ Log.d(TAG, "postponed state after:");
+ for (UpdateOp updateOp : mPostponedList) {
+ Log.d(TAG, updateOp.toString());
+ }
+ Log.d(TAG, "----");
+ }
+ }
+
+ void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
+ mCallback.onDispatchFirstPass(op);
+ switch (op.cmd) {
+ case UpdateOp.REMOVE:
+ mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
+ break;
+ case UpdateOp.UPDATE:
+ mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload);
+ break;
+ default:
+ throw new IllegalArgumentException("only remove and update ops can be dispatched"
+ + " in first pass");
+ }
+ }
+
+ private int updatePositionWithPostponed(int pos, int cmd) {
+ final int count = mPostponedList.size();
+ for (int i = count - 1; i >= 0; i--) {
+ UpdateOp postponed = mPostponedList.get(i);
+ if (postponed.cmd == UpdateOp.MOVE) {
+ int start, end;
+ if (postponed.positionStart < postponed.itemCount) {
+ start = postponed.positionStart;
+ end = postponed.itemCount;
+ } else {
+ start = postponed.itemCount;
+ end = postponed.positionStart;
+ }
+ if (pos >= start && pos <= end) {
+ //i'm affected
+ if (start == postponed.positionStart) {
+ if (cmd == UpdateOp.ADD) {
+ postponed.itemCount++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.itemCount--;
+ }
+ // op moved to left, move it right to revert
+ pos++;
+ } else {
+ if (cmd == UpdateOp.ADD) {
+ postponed.positionStart++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.positionStart--;
+ }
+ // op was moved right, move left to revert
+ pos--;
+ }
+ } else if (pos < postponed.positionStart) {
+ // postponed MV is outside the dispatched OP. if it is before, offset
+ if (cmd == UpdateOp.ADD) {
+ postponed.positionStart++;
+ postponed.itemCount++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.positionStart--;
+ postponed.itemCount--;
+ }
+ }
+ } else {
+ if (postponed.positionStart <= pos) {
+ if (postponed.cmd == UpdateOp.ADD) {
+ pos -= postponed.itemCount;
+ } else if (postponed.cmd == UpdateOp.REMOVE) {
+ pos += postponed.itemCount;
+ }
+ } else {
+ if (cmd == UpdateOp.ADD) {
+ postponed.positionStart++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.positionStart--;
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "dispath (step" + i + ")");
+ Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
+ for (UpdateOp updateOp : mPostponedList) {
+ Log.d(TAG, updateOp.toString());
+ }
+ Log.d(TAG, "----");
+ }
+ }
+ for (int i = mPostponedList.size() - 1; i >= 0; i--) {
+ UpdateOp op = mPostponedList.get(i);
+ if (op.cmd == UpdateOp.MOVE) {
+ if (op.itemCount == op.positionStart || op.itemCount < 0) {
+ mPostponedList.remove(i);
+ recycleUpdateOp(op);
+ }
+ } else if (op.itemCount <= 0) {
+ mPostponedList.remove(i);
+ recycleUpdateOp(op);
+ }
+ }
+ return pos;
+ }
+
+ private boolean canFindInPreLayout(int position) {
+ final int count = mPostponedList.size();
+ for (int i = 0; i < count; i++) {
+ UpdateOp op = mPostponedList.get(i);
+ if (op.cmd == UpdateOp.MOVE) {
+ if (findPositionOffset(op.itemCount, i + 1) == position) {
+ return true;
+ }
+ } else if (op.cmd == UpdateOp.ADD) {
+ // TODO optimize.
+ final int end = op.positionStart + op.itemCount;
+ for (int pos = op.positionStart; pos < end; pos++) {
+ if (findPositionOffset(pos, i + 1) == position) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private void applyAdd(UpdateOp op) {
+ postponeAndUpdateViewHolders(op);
+ }
+
+ private void postponeAndUpdateViewHolders(UpdateOp op) {
+ if (DEBUG) {
+ Log.d(TAG, "postponing " + op);
+ }
+ mPostponedList.add(op);
+ switch (op.cmd) {
+ case UpdateOp.ADD:
+ mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
+ break;
+ case UpdateOp.MOVE:
+ mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
+ break;
+ case UpdateOp.REMOVE:
+ mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
+ op.itemCount);
+ break;
+ case UpdateOp.UPDATE:
+ mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown update op type for " + op);
+ }
+ }
+
+ boolean hasPendingUpdates() {
+ return mPendingUpdates.size() > 0;
+ }
+
+ boolean hasAnyUpdateTypes(int updateTypes) {
+ return (mExistingUpdateTypes & updateTypes) != 0;
+ }
+
+ int findPositionOffset(int position) {
+ return findPositionOffset(position, 0);
+ }
+
+ int findPositionOffset(int position, int firstPostponedItem) {
+ int count = mPostponedList.size();
+ for (int i = firstPostponedItem; i < count; ++i) {
+ UpdateOp op = mPostponedList.get(i);
+ if (op.cmd == UpdateOp.MOVE) {
+ if (op.positionStart == position) {
+ position = op.itemCount;
+ } else {
+ if (op.positionStart < position) {
+ position--; // like a remove
+ }
+ if (op.itemCount <= position) {
+ position++; // like an add
+ }
+ }
+ } else if (op.positionStart <= position) {
+ if (op.cmd == UpdateOp.REMOVE) {
+ if (position < op.positionStart + op.itemCount) {
+ return -1;
+ }
+ position -= op.itemCount;
+ } else if (op.cmd == UpdateOp.ADD) {
+ position += op.itemCount;
+ }
+ }
+ }
+ return position;
+ }
+
+ /**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ if (itemCount < 1) {
+ return false;
+ }
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
+ mExistingUpdateTypes |= UpdateOp.UPDATE;
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeInserted(int positionStart, int itemCount) {
+ if (itemCount < 1) {
+ return false;
+ }
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
+ mExistingUpdateTypes |= UpdateOp.ADD;
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeRemoved(int positionStart, int itemCount) {
+ if (itemCount < 1) {
+ return false;
+ }
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
+ mExistingUpdateTypes |= UpdateOp.REMOVE;
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeMoved(int from, int to, int itemCount) {
+ if (from == to) {
+ return false; // no-op
+ }
+ if (itemCount != 1) {
+ throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
+ }
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to, null));
+ mExistingUpdateTypes |= UpdateOp.MOVE;
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
+ * Skips pre-processing and applies all updates in one pass.
+ */
+ void consumeUpdatesInOnePass() {
+ // we still consume postponed updates (if there is) in case there was a pre-process call
+ // w/o a matching consumePostponedUpdates.
+ consumePostponedUpdates();
+ final int count = mPendingUpdates.size();
+ for (int i = 0; i < count; i++) {
+ UpdateOp op = mPendingUpdates.get(i);
+ switch (op.cmd) {
+ case UpdateOp.ADD:
+ mCallback.onDispatchSecondPass(op);
+ mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
+ break;
+ case UpdateOp.REMOVE:
+ mCallback.onDispatchSecondPass(op);
+ mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
+ break;
+ case UpdateOp.UPDATE:
+ mCallback.onDispatchSecondPass(op);
+ mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
+ break;
+ case UpdateOp.MOVE:
+ mCallback.onDispatchSecondPass(op);
+ mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
+ break;
+ }
+ if (mOnItemProcessedCallback != null) {
+ mOnItemProcessedCallback.run();
+ }
+ }
+ recycleUpdateOpsAndClearList(mPendingUpdates);
+ mExistingUpdateTypes = 0;
+ }
+
+ public int applyPendingUpdatesToPosition(int position) {
+ final int size = mPendingUpdates.size();
+ for (int i = 0; i < size; i++) {
+ UpdateOp op = mPendingUpdates.get(i);
+ switch (op.cmd) {
+ case UpdateOp.ADD:
+ if (op.positionStart <= position) {
+ position += op.itemCount;
+ }
+ break;
+ case UpdateOp.REMOVE:
+ if (op.positionStart <= position) {
+ final int end = op.positionStart + op.itemCount;
+ if (end > position) {
+ return RecyclerView.NO_POSITION;
+ }
+ position -= op.itemCount;
+ }
+ break;
+ case UpdateOp.MOVE:
+ if (op.positionStart == position) {
+ position = op.itemCount; //position end
+ } else {
+ if (op.positionStart < position) {
+ position -= 1;
+ }
+ if (op.itemCount <= position) {
+ position += 1;
+ }
+ }
+ break;
+ }
+ }
+ return position;
+ }
+
+ boolean hasUpdates() {
+ return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty();
+ }
+
+ /**
+ * Queued operation to happen when child views are updated.
+ */
+ static class UpdateOp {
+
+ static final int ADD = 1;
+
+ static final int REMOVE = 1 << 1;
+
+ static final int UPDATE = 1 << 2;
+
+ static final int MOVE = 1 << 3;
+
+ static final int POOL_SIZE = 30;
+
+ int cmd;
+
+ int positionStart;
+
+ Object payload;
+
+ // holds the target position if this is a MOVE
+ int itemCount;
+
+ UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
+ this.cmd = cmd;
+ this.positionStart = positionStart;
+ this.itemCount = itemCount;
+ this.payload = payload;
+ }
+
+ String cmdToString() {
+ switch (cmd) {
+ case ADD:
+ return "add";
+ case REMOVE:
+ return "rm";
+ case UPDATE:
+ return "up";
+ case MOVE:
+ return "mv";
+ }
+ return "??";
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toHexString(System.identityHashCode(this))
+ + "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount
+ + ",p:" + payload + "]";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ UpdateOp op = (UpdateOp) o;
+
+ if (cmd != op.cmd) {
+ return false;
+ }
+ if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
+ // reverse of this is also true
+ if (itemCount == op.positionStart && positionStart == op.itemCount) {
+ return true;
+ }
+ }
+ if (itemCount != op.itemCount) {
+ return false;
+ }
+ if (positionStart != op.positionStart) {
+ return false;
+ }
+ if (payload != null) {
+ if (!payload.equals(op.payload)) {
+ return false;
+ }
+ } else if (op.payload != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = cmd;
+ result = 31 * result + positionStart;
+ result = 31 * result + itemCount;
+ return result;
+ }
+ }
+
+ @Override
+ public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
+ UpdateOp op = mUpdateOpPool.acquire();
+ if (op == null) {
+ op = new UpdateOp(cmd, positionStart, itemCount, payload);
+ } else {
+ op.cmd = cmd;
+ op.positionStart = positionStart;
+ op.itemCount = itemCount;
+ op.payload = payload;
+ }
+ return op;
+ }
+
+ @Override
+ public void recycleUpdateOp(UpdateOp op) {
+ if (!mDisableRecycler) {
+ op.payload = null;
+ mUpdateOpPool.release(op);
+ }
+ }
+
+ void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
+ final int count = ops.size();
+ for (int i = 0; i < count; i++) {
+ recycleUpdateOp(ops.get(i));
+ }
+ ops.clear();
+ }
+
+ /**
+ * Contract between AdapterHelper and RecyclerView.
+ */
+ interface Callback {
+
+ RecyclerView.ViewHolder findViewHolder(int position);
+
+ void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
+
+ void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
+
+ void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads);
+
+ void onDispatchFirstPass(UpdateOp updateOp);
+
+ void onDispatchSecondPass(UpdateOp updateOp);
+
+ void offsetPositionsForAdd(int positionStart, int itemCount);
+
+ void offsetPositionsForMove(int from, int to);
+ }
+}
diff --git a/com/android/internal/widget/AlertDialogLayout.java b/com/android/internal/widget/AlertDialogLayout.java
new file mode 100644
index 0000000..d879b6d
--- /dev/null
+++ b/com/android/internal/widget/AlertDialogLayout.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.android.internal.R;
+
+/**
+ * Special implementation of linear layout that's capable of laying out alert
+ * dialog components.
+ * <p>
+ * A dialog consists of up to three panels. All panels are optional, and a
+ * dialog may contain only a single panel. The panels are laid out according
+ * to the following guidelines:
+ * <ul>
+ * <li>topPanel: exactly wrap_content</li>
+ * <li>contentPanel OR customPanel: at most fill_parent, first priority for
+ * extra space</li>
+ * <li>buttonPanel: at least minHeight, at most wrap_content, second
+ * priority for extra space</li>
+ * </ul>
+ */
+public class AlertDialogLayout extends LinearLayout {
+
+ public AlertDialogLayout(@Nullable Context context) {
+ super(context);
+ }
+
+ @UnsupportedAppUsage
+ public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) {
+ // Failed to perform custom measurement, let superclass handle it.
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ View topPanel = null;
+ View buttonPanel = null;
+ View middlePanel = null;
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == View.GONE) {
+ continue;
+ }
+
+ final int id = child.getId();
+ switch (id) {
+ case R.id.topPanel:
+ topPanel = child;
+ break;
+ case R.id.buttonPanel:
+ buttonPanel = child;
+ break;
+ case R.id.contentPanel:
+ case R.id.customPanel:
+ if (middlePanel != null) {
+ // Both the content and custom are visible. Abort!
+ return false;
+ }
+ middlePanel = child;
+ break;
+ default:
+ // Unknown top-level child. Abort!
+ return false;
+ }
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+
+ int childState = 0;
+ int usedHeight = getPaddingTop() + getPaddingBottom();
+
+ if (topPanel != null) {
+ topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
+
+ usedHeight += topPanel.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, topPanel.getMeasuredState());
+ }
+
+ int buttonHeight = 0;
+ int buttonWantsHeight = 0;
+ if (buttonPanel != null) {
+ buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
+ buttonHeight = resolveMinimumHeight(buttonPanel);
+ buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight;
+
+ usedHeight += buttonHeight;
+ childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
+ }
+
+ int middleHeight = 0;
+ if (middlePanel != null) {
+ final int childHeightSpec;
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ childHeightSpec = MeasureSpec.UNSPECIFIED;
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, heightSize - usedHeight), heightMode);
+ }
+
+ middlePanel.measure(widthMeasureSpec, childHeightSpec);
+ middleHeight = middlePanel.getMeasuredHeight();
+
+ usedHeight += middleHeight;
+ childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
+ }
+
+ int remainingHeight = heightSize - usedHeight;
+
+ // Time for the "real" button measure pass. If we have remaining space,
+ // make the button pane bigger up to its target height. Otherwise,
+ // just remeasure the button at whatever height it needs.
+ if (buttonPanel != null) {
+ usedHeight -= buttonHeight;
+
+ final int heightToGive = Math.min(remainingHeight, buttonWantsHeight);
+ if (heightToGive > 0) {
+ remainingHeight -= heightToGive;
+ buttonHeight += heightToGive;
+ }
+
+ final int childHeightSpec = MeasureSpec.makeMeasureSpec(
+ buttonHeight, MeasureSpec.EXACTLY);
+ buttonPanel.measure(widthMeasureSpec, childHeightSpec);
+
+ usedHeight += buttonPanel.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
+ }
+
+ // If we still have remaining space, make the middle pane bigger up
+ // to the maximum height.
+ if (middlePanel != null && remainingHeight > 0) {
+ usedHeight -= middleHeight;
+
+ final int heightToGive = remainingHeight;
+ remainingHeight -= heightToGive;
+ middleHeight += heightToGive;
+
+ // Pass the same height mode as we're using for the dialog itself.
+ // If it's EXACTLY, then the middle pane MUST use the entire
+ // height.
+ final int childHeightSpec = MeasureSpec.makeMeasureSpec(
+ middleHeight, heightMode);
+ middlePanel.measure(widthMeasureSpec, childHeightSpec);
+
+ usedHeight += middlePanel.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
+ }
+
+ // Compute desired width as maximum child width.
+ int maxWidth = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE) {
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
+ }
+ }
+
+ maxWidth += getPaddingLeft() + getPaddingRight();
+
+ final int widthSizeAndState = resolveSizeAndState(maxWidth, widthMeasureSpec, childState);
+ final int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0);
+ setMeasuredDimension(widthSizeAndState, heightSizeAndState);
+
+ // If the children weren't already measured EXACTLY, we need to run
+ // another measure pass to for MATCH_PARENT widths.
+ if (widthMode != MeasureSpec.EXACTLY) {
+ forceUniformWidth(count, heightMeasureSpec);
+ }
+
+ return true;
+ }
+
+ /**
+ * Remeasures child views to exactly match the layout's measured width.
+ *
+ * @param count the number of child views
+ * @param heightMeasureSpec the original height measure spec
+ */
+ private void forceUniformWidth(int count, int heightMeasureSpec) {
+ // Pretend that the linear layout has an exact size.
+ final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(
+ getMeasuredWidth(), MeasureSpec.EXACTLY);
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ // Temporarily force children to reuse their old measured
+ // height.
+ final int oldHeight = lp.height;
+ lp.height = child.getMeasuredHeight();
+
+ // Remeasure with new dimensions.
+ measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
+ lp.height = oldHeight;
+ }
+ }
+ }
+ }
+
+ /**
+ * Attempts to resolve the minimum height of a view.
+ * <p>
+ * If the view doesn't have a minimum height set and only contains a single
+ * child, attempts to resolve the minimum height of the child view.
+ *
+ * @param v the view whose minimum height to resolve
+ * @return the minimum height
+ */
+ private int resolveMinimumHeight(View v) {
+ final int minHeight = v.getMinimumHeight();
+ if (minHeight > 0) {
+ return minHeight;
+ }
+
+ if (v instanceof ViewGroup) {
+ final ViewGroup vg = (ViewGroup) v;
+ if (vg.getChildCount() == 1) {
+ return resolveMinimumHeight(vg.getChildAt(0));
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int paddingLeft = mPaddingLeft;
+
+ // Where right end of child should go
+ final int width = right - left;
+ final int childRight = width - mPaddingRight;
+
+ // Space available for child
+ final int childSpace = width - paddingLeft - mPaddingRight;
+
+ final int totalLength = getMeasuredHeight();
+ final int count = getChildCount();
+ final int gravity = getGravity();
+ final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int minorGravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+
+ int childTop;
+ switch (majorGravity) {
+ case Gravity.BOTTOM:
+ // totalLength contains the padding already
+ childTop = mPaddingTop + bottom - top - totalLength;
+ break;
+
+ // totalLength contains the padding already
+ case Gravity.CENTER_VERTICAL:
+ childTop = mPaddingTop + (bottom - top - totalLength) / 2;
+ break;
+
+ case Gravity.TOP:
+ default:
+ childTop = mPaddingTop;
+ break;
+ }
+
+ final Drawable dividerDrawable = getDividerDrawable();
+ final int dividerHeight = dividerDrawable == null ?
+ 0 : dividerDrawable.getIntrinsicHeight();
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ final LinearLayout.LayoutParams lp =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ int layoutGravity = lp.gravity;
+ if (layoutGravity < 0) {
+ layoutGravity = minorGravity;
+ }
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(
+ layoutGravity, layoutDirection);
+
+ final int childLeft;
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ + lp.leftMargin - lp.rightMargin;
+ break;
+
+ case Gravity.RIGHT:
+ childLeft = childRight - childWidth - lp.rightMargin;
+ break;
+
+ case Gravity.LEFT:
+ default:
+ childLeft = paddingLeft + lp.leftMargin;
+ break;
+ }
+
+ if (hasDividerBeforeChildAt(i)) {
+ childTop += dividerHeight;
+ }
+
+ childTop += lp.topMargin;
+ setChildFrame(child, childLeft, childTop, childWidth, childHeight);
+ childTop += childHeight + lp.bottomMargin;
+ }
+ }
+ }
+
+ private void setChildFrame(View child, int left, int top, int width, int height) {
+ child.layout(left, top, left + width, top + height);
+ }
+}
diff --git a/com/android/internal/widget/AutoScrollHelper.java b/com/android/internal/widget/AutoScrollHelper.java
new file mode 100644
index 0000000..0d468ca
--- /dev/null
+++ b/com/android/internal/widget/AutoScrollHelper.java
@@ -0,0 +1,928 @@
+/*
+ * Copyright (C) 2013 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.internal.widget;
+
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.AbsListView;
+
+/**
+ * AutoScrollHelper is a utility class for adding automatic edge-triggered
+ * scrolling to Views.
+ * <p>
+ * <b>Note:</b> Implementing classes are responsible for overriding the
+ * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and
+ * {@link #canTargetScrollVertically} methods. See
+ * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
+ * -specific implementation.
+ * <p>
+ * <h1>Activation</h1> Automatic scrolling starts when the user touches within
+ * an activation area. By default, activation areas are defined as the top,
+ * left, right, and bottom 20% of the host view's total area. Touching within
+ * the top activation area scrolls up, left scrolls to the left, and so on.
+ * <p>
+ * As the user touches closer to the extreme edge of the activation area,
+ * scrolling accelerates up to a maximum velocity. When using the default edge
+ * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds
+ * will scroll at the maximum velocity.
+ * <p>
+ * The following activation properties may be configured:
+ * <ul>
+ * <li>Delay after entering activation area before auto-scrolling begins, see
+ * {@link #setActivationDelay}. Default value is
+ * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps.
+ * <li>Location of activation areas, see {@link #setEdgeType}. Default value is
+ * {@link #EDGE_TYPE_INSIDE_EXTEND}.
+ * <li>Size of activation areas relative to view size, see
+ * {@link #setRelativeEdges}. Default value is 20% for both vertical and
+ * horizontal edges.
+ * <li>Maximum size used to constrain relative size, see
+ * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
+ * </ul>
+ * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
+ * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets.
+ * <p>
+ * The following scrolling properties may be configured:
+ * <ul>
+ * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default
+ * value is 500 milliseconds.
+ * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}.
+ * Default value is 500 milliseconds.
+ * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}.
+ * Default value is 100% per second for both vertical and horizontal.
+ * <li>Minimum velocity used to constrain relative velocity, see
+ * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the
+ * larger of either this value or the relative target value. Default value is
+ * approximately 5 centimeters or 315 dips per second.
+ * <li>Maximum velocity used to constrain relative velocity, see
+ * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or
+ * 1575 dips per second.
+ * </ul>
+ */
+public abstract class AutoScrollHelper implements View.OnTouchListener {
+ /**
+ * Constant passed to {@link #setRelativeEdges} or
+ * {@link #setRelativeVelocity}. Using this value ensures that the computed
+ * relative value is ignored and the absolute maximum value is always used.
+ */
+ public static final float RELATIVE_UNSPECIFIED = 0;
+
+ /**
+ * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity},
+ * or {@link #setMinimumVelocity}. Using this value ensures that the
+ * computed relative value is always used without constraining to a
+ * particular minimum or maximum value.
+ */
+ public static final float NO_MAX = Float.MAX_VALUE;
+
+ /**
+ * Constant passed to {@link #setMaximumEdges}, or
+ * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this
+ * value ensures that the computed relative value is always used without
+ * constraining to a particular minimum or maximum value.
+ */
+ public static final float NO_MIN = 0;
+
+ /**
+ * Edge type that specifies an activation area starting at the view bounds
+ * and extending inward. Moving outside the view bounds will stop scrolling.
+ *
+ * @see #setEdgeType
+ */
+ public static final int EDGE_TYPE_INSIDE = 0;
+
+ /**
+ * Edge type that specifies an activation area starting at the view bounds
+ * and extending inward. After activation begins, moving outside the view
+ * bounds will continue scrolling.
+ *
+ * @see #setEdgeType
+ */
+ public static final int EDGE_TYPE_INSIDE_EXTEND = 1;
+
+ /**
+ * Edge type that specifies an activation area starting at the view bounds
+ * and extending outward. Moving inside the view bounds will stop scrolling.
+ *
+ * @see #setEdgeType
+ */
+ public static final int EDGE_TYPE_OUTSIDE = 2;
+
+ private static final int HORIZONTAL = 0;
+ private static final int VERTICAL = 1;
+
+ /** Scroller used to control acceleration toward maximum velocity. */
+ private final ClampedScroller mScroller = new ClampedScroller();
+
+ /** Interpolator used to scale velocity with touch position. */
+ private final Interpolator mEdgeInterpolator = new AccelerateInterpolator();
+
+ /** The view to auto-scroll. Might not be the source of touch events. */
+ private final View mTarget;
+
+ /** Runnable used to animate scrolling. */
+ private Runnable mRunnable;
+
+ /** Edge insets used to activate auto-scrolling. */
+ private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
+
+ /** Clamping values for edge insets used to activate auto-scrolling. */
+ private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX };
+
+ /** The type of edge being used. */
+ private int mEdgeType;
+
+ /** Delay after entering an activation edge before auto-scrolling begins. */
+ private int mActivationDelay;
+
+ /** Relative scrolling velocity at maximum edge distance. */
+ private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
+
+ /** Clamping values used for scrolling velocity. */
+ private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN };
+
+ /** Clamping values used for scrolling velocity. */
+ private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };
+
+ /** Whether to start activation immediately. */
+ private boolean mAlreadyDelayed;
+
+ /** Whether to reset the scroller start time on the next animation. */
+ private boolean mNeedsReset;
+
+ /** Whether to send a cancel motion event to the target view. */
+ private boolean mNeedsCancel;
+
+ /** Whether the auto-scroller is actively scrolling. */
+ private boolean mAnimating;
+
+ /** Whether the auto-scroller is enabled. */
+ private boolean mEnabled;
+
+ /** Whether the auto-scroller consumes events when scrolling. */
+ private boolean mExclusive;
+
+ // Default values.
+ private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND;
+ private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315;
+ private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575;
+ private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX;
+ private static final float DEFAULT_RELATIVE_EDGE = 0.2f;
+ private static final float DEFAULT_RELATIVE_VELOCITY = 1f;
+ private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout();
+ private static final int DEFAULT_RAMP_UP_DURATION = 500;
+ private static final int DEFAULT_RAMP_DOWN_DURATION = 500;
+
+ /**
+ * Creates a new helper for scrolling the specified target view.
+ * <p>
+ * The resulting helper may be configured by chaining setter calls and
+ * should be set as a touch listener on the target view.
+ * <p>
+ * By default, the helper is disabled and will not respond to touch events
+ * until it is enabled using {@link #setEnabled}.
+ *
+ * @param target The view to automatically scroll.
+ */
+ public AutoScrollHelper(View target) {
+ mTarget = target;
+
+ final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
+ final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
+ final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
+ setMaximumVelocity(maxVelocity, maxVelocity);
+ setMinimumVelocity(minVelocity, minVelocity);
+
+ setEdgeType(DEFAULT_EDGE_TYPE);
+ setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE);
+ setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE);
+ setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY);
+ setActivationDelay(DEFAULT_ACTIVATION_DELAY);
+ setRampUpDuration(DEFAULT_RAMP_UP_DURATION);
+ setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION);
+ }
+
+ /**
+ * Sets whether the scroll helper is enabled and should respond to touch
+ * events.
+ *
+ * @param enabled Whether the scroll helper is enabled.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setEnabled(boolean enabled) {
+ if (mEnabled && !enabled) {
+ requestStop();
+ }
+
+ mEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * @return True if this helper is enabled and responding to touch events.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Enables or disables exclusive handling of touch events during scrolling.
+ * By default, exclusive handling is disabled and the target view receives
+ * all touch events.
+ * <p>
+ * When enabled, {@link #onTouch} will return true if the helper is
+ * currently scrolling and false otherwise.
+ *
+ * @param exclusive True to exclusively handle touch events during scrolling,
+ * false to allow the target view to receive all touch events.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setExclusive(boolean exclusive) {
+ mExclusive = exclusive;
+ return this;
+ }
+
+ /**
+ * Indicates whether the scroll helper handles touch events exclusively
+ * during scrolling.
+ *
+ * @return True if exclusive handling of touch events during scrolling is
+ * enabled, false otherwise.
+ * @see #setExclusive(boolean)
+ */
+ public boolean isExclusive() {
+ return mExclusive;
+ }
+
+ /**
+ * Sets the absolute maximum scrolling velocity.
+ * <p>
+ * If relative velocity is not specified, scrolling will always reach the
+ * same maximum velocity. If both relative and maximum velocities are
+ * specified, the maximum velocity will be used to clamp the calculated
+ * relative velocity.
+ *
+ * @param horizontalMax The maximum horizontal scrolling velocity, or
+ * {@link #NO_MAX} to leave the relative value unconstrained.
+ * @param verticalMax The maximum vertical scrolling velocity, or
+ * {@link #NO_MAX} to leave the relative value unconstrained.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
+ mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f;
+ mMaximumVelocity[VERTICAL] = verticalMax / 1000f;
+ return this;
+ }
+
+ /**
+ * Sets the absolute minimum scrolling velocity.
+ * <p>
+ * If both relative and minimum velocities are specified, the minimum
+ * velocity will be used to clamp the calculated relative velocity.
+ *
+ * @param horizontalMin The minimum horizontal scrolling velocity, or
+ * {@link #NO_MIN} to leave the relative value unconstrained.
+ * @param verticalMin The minimum vertical scrolling velocity, or
+ * {@link #NO_MIN} to leave the relative value unconstrained.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
+ mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f;
+ mMinimumVelocity[VERTICAL] = verticalMin / 1000f;
+ return this;
+ }
+
+ /**
+ * Sets the target scrolling velocity relative to the host view's
+ * dimensions.
+ * <p>
+ * If both relative and maximum velocities are specified, the maximum
+ * velocity will be used to clamp the calculated relative velocity.
+ *
+ * @param horizontal The target horizontal velocity as a fraction of the
+ * host view width per second, or {@link #RELATIVE_UNSPECIFIED}
+ * to ignore.
+ * @param vertical The target vertical velocity as a fraction of the host
+ * view height per second, or {@link #RELATIVE_UNSPECIFIED} to
+ * ignore.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
+ mRelativeVelocity[HORIZONTAL] = horizontal / 1000f;
+ mRelativeVelocity[VERTICAL] = vertical / 1000f;
+ return this;
+ }
+
+ /**
+ * Sets the activation edge type, one of:
+ * <ul>
+ * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside
+ * the bounds of the host view. If touch moves outside the bounds, scrolling
+ * will stop.
+ * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to
+ * scroll when touch moves outside the bounds of the host view.
+ * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches
+ * that move outside the bounds of the host view.
+ * </ul>
+ *
+ * @param type The type of edge to use.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setEdgeType(int type) {
+ mEdgeType = type;
+ return this;
+ }
+
+ /**
+ * Sets the activation edge size relative to the host view's dimensions.
+ * <p>
+ * If both relative and maximum edges are specified, the maximum edge will
+ * be used to constrain the calculated relative edge size.
+ *
+ * @param horizontal The horizontal edge size as a fraction of the host view
+ * width, or {@link #RELATIVE_UNSPECIFIED} to always use the
+ * maximum value.
+ * @param vertical The vertical edge size as a fraction of the host view
+ * height, or {@link #RELATIVE_UNSPECIFIED} to always use the
+ * maximum value.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
+ mRelativeEdges[HORIZONTAL] = horizontal;
+ mRelativeEdges[VERTICAL] = vertical;
+ return this;
+ }
+
+ /**
+ * Sets the absolute maximum edge size.
+ * <p>
+ * If relative edge size is not specified, activation edges will always be
+ * the maximum edge size. If both relative and maximum edges are specified,
+ * the maximum edge will be used to constrain the calculated relative edge
+ * size.
+ *
+ * @param horizontalMax The maximum horizontal edge size in pixels, or
+ * {@link #NO_MAX} to use the unconstrained calculated relative
+ * value.
+ * @param verticalMax The maximum vertical edge size in pixels, or
+ * {@link #NO_MAX} to use the unconstrained calculated relative
+ * value.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
+ mMaximumEdges[HORIZONTAL] = horizontalMax;
+ mMaximumEdges[VERTICAL] = verticalMax;
+ return this;
+ }
+
+ /**
+ * Sets the delay after entering an activation edge before activation of
+ * auto-scrolling. By default, the activation delay is set to
+ * {@link ViewConfiguration#getTapTimeout()}.
+ * <p>
+ * Specifying a delay of zero will start auto-scrolling immediately after
+ * the touch position enters an activation edge.
+ *
+ * @param delayMillis The activation delay in milliseconds.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setActivationDelay(int delayMillis) {
+ mActivationDelay = delayMillis;
+ return this;
+ }
+
+ /**
+ * Sets the amount of time after activation of auto-scrolling that is takes
+ * to reach target velocity for the current touch position.
+ * <p>
+ * Specifying a duration greater than zero prevents sudden jumps in
+ * velocity.
+ *
+ * @param durationMillis The ramp-up duration in milliseconds.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setRampUpDuration(int durationMillis) {
+ mScroller.setRampUpDuration(durationMillis);
+ return this;
+ }
+
+ /**
+ * Sets the amount of time after de-activation of auto-scrolling that is
+ * takes to slow to a stop.
+ * <p>
+ * Specifying a duration greater than zero prevents sudden jumps in
+ * velocity.
+ *
+ * @param durationMillis The ramp-down duration in milliseconds.
+ * @return The scroll helper, which may used to chain setter calls.
+ */
+ public AutoScrollHelper setRampDownDuration(int durationMillis) {
+ mScroller.setRampDownDuration(durationMillis);
+ return this;
+ }
+
+ /**
+ * Handles touch events by activating automatic scrolling, adjusting scroll
+ * velocity, or stopping.
+ * <p>
+ * If {@link #isExclusive()} is false, always returns false so that
+ * the host view may handle touch events. Otherwise, returns true when
+ * automatic scrolling is active and false otherwise.
+ */
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (!mEnabled) {
+ return false;
+ }
+
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mNeedsCancel = true;
+ mAlreadyDelayed = false;
+ // $FALL-THROUGH$
+ case MotionEvent.ACTION_MOVE:
+ final float xTargetVelocity = computeTargetVelocity(
+ HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth());
+ final float yTargetVelocity = computeTargetVelocity(
+ VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight());
+ mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity);
+
+ // If the auto scroller was not previously active, but it should
+ // be, then update the state and start animations.
+ if (!mAnimating && shouldAnimate()) {
+ startAnimating();
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ requestStop();
+ break;
+ }
+
+ return mExclusive && mAnimating;
+ }
+
+ /**
+ * @return whether the target is able to scroll in the requested direction
+ */
+ private boolean shouldAnimate() {
+ final ClampedScroller scroller = mScroller;
+ final int verticalDirection = scroller.getVerticalDirection();
+ final int horizontalDirection = scroller.getHorizontalDirection();
+
+ return verticalDirection != 0 && canTargetScrollVertically(verticalDirection)
+ || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection);
+ }
+
+ /**
+ * Starts the scroll animation.
+ */
+ private void startAnimating() {
+ if (mRunnable == null) {
+ mRunnable = new ScrollAnimationRunnable();
+ }
+
+ mAnimating = true;
+ mNeedsReset = true;
+
+ if (!mAlreadyDelayed && mActivationDelay > 0) {
+ mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay);
+ } else {
+ mRunnable.run();
+ }
+
+ // If we start animating again before the user lifts their finger, we
+ // already know it's not a tap and don't need an activation delay.
+ mAlreadyDelayed = true;
+ }
+
+ /**
+ * Requests that the scroll animation slow to a stop. If there is an
+ * activation delay, this may occur between posting the animation and
+ * actually running it.
+ */
+ private void requestStop() {
+ if (mNeedsReset) {
+ // The animation has been posted, but hasn't run yet. Manually
+ // stopping animation will prevent it from running.
+ mAnimating = false;
+ } else {
+ mScroller.requestStop();
+ }
+ }
+
+ private float computeTargetVelocity(
+ int direction, float coordinate, float srcSize, float dstSize) {
+ final float relativeEdge = mRelativeEdges[direction];
+ final float maximumEdge = mMaximumEdges[direction];
+ final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate);
+ if (value == 0) {
+ // The edge in this direction is not activated.
+ return 0;
+ }
+
+ final float relativeVelocity = mRelativeVelocity[direction];
+ final float minimumVelocity = mMinimumVelocity[direction];
+ final float maximumVelocity = mMaximumVelocity[direction];
+ final float targetVelocity = relativeVelocity * dstSize;
+
+ // Target velocity is adjusted for interpolated edge position, then
+ // clamped to the minimum and maximum values. Later, this value will be
+ // adjusted for time-based acceleration.
+ if (value > 0) {
+ return constrain(value * targetVelocity, minimumVelocity, maximumVelocity);
+ } else {
+ return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity);
+ }
+ }
+
+ /**
+ * Override this method to scroll the target view by the specified number of
+ * pixels.
+ *
+ * @param deltaX The number of pixels to scroll by horizontally.
+ * @param deltaY The number of pixels to scroll by vertically.
+ */
+ public abstract void scrollTargetBy(int deltaX, int deltaY);
+
+ /**
+ * Override this method to return whether the target view can be scrolled
+ * horizontally in a certain direction.
+ *
+ * @param direction Negative to check scrolling left, positive to check
+ * scrolling right.
+ * @return true if the target view is able to horizontally scroll in the
+ * specified direction.
+ */
+ public abstract boolean canTargetScrollHorizontally(int direction);
+
+ /**
+ * Override this method to return whether the target view can be scrolled
+ * vertically in a certain direction.
+ *
+ * @param direction Negative to check scrolling up, positive to check
+ * scrolling down.
+ * @return true if the target view is able to vertically scroll in the
+ * specified direction.
+ */
+ public abstract boolean canTargetScrollVertically(int direction);
+
+ /**
+ * Returns the interpolated position of a touch point relative to an edge
+ * defined by its relative inset, its maximum absolute inset, and the edge
+ * interpolator.
+ *
+ * @param relativeValue The size of the inset relative to the total size.
+ * @param size Total size.
+ * @param maxValue The maximum size of the inset, used to clamp (relative *
+ * total).
+ * @param current Touch position within within the total size.
+ * @return Interpolated value of the touch position within the edge.
+ */
+ private float getEdgeValue(float relativeValue, float size, float maxValue, float current) {
+ // For now, leading and trailing edges are always the same size.
+ final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue);
+ final float valueLeading = constrainEdgeValue(current, edgeSize);
+ final float valueTrailing = constrainEdgeValue(size - current, edgeSize);
+ final float value = (valueTrailing - valueLeading);
+ final float interpolated;
+ if (value < 0) {
+ interpolated = -mEdgeInterpolator.getInterpolation(-value);
+ } else if (value > 0) {
+ interpolated = mEdgeInterpolator.getInterpolation(value);
+ } else {
+ return 0;
+ }
+
+ return constrain(interpolated, -1, 1);
+ }
+
+ private float constrainEdgeValue(float current, float leading) {
+ if (leading == 0) {
+ return 0;
+ }
+
+ switch (mEdgeType) {
+ case EDGE_TYPE_INSIDE:
+ case EDGE_TYPE_INSIDE_EXTEND:
+ if (current < leading) {
+ if (current >= 0) {
+ // Movement up to the edge is scaled.
+ return 1f - current / leading;
+ } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
+ // Movement beyond the edge is always maximum.
+ return 1f;
+ }
+ }
+ break;
+ case EDGE_TYPE_OUTSIDE:
+ if (current < 0) {
+ // Movement beyond the edge is scaled.
+ return current / -leading;
+ }
+ break;
+ }
+
+ return 0;
+ }
+
+ private static int constrain(int value, int min, int max) {
+ if (value > max) {
+ return max;
+ } else if (value < min) {
+ return min;
+ } else {
+ return value;
+ }
+ }
+
+ private static float constrain(float value, float min, float max) {
+ if (value > max) {
+ return max;
+ } else if (value < min) {
+ return min;
+ } else {
+ return value;
+ }
+ }
+
+ /**
+ * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view,
+ * canceling any ongoing touch events.
+ */
+ private void cancelTargetTouch() {
+ final long eventTime = SystemClock.uptimeMillis();
+ final MotionEvent cancel = MotionEvent.obtain(
+ eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mTarget.onTouchEvent(cancel);
+ cancel.recycle();
+ }
+
+ private class ScrollAnimationRunnable implements Runnable {
+ @Override
+ public void run() {
+ if (!mAnimating) {
+ return;
+ }
+
+ if (mNeedsReset) {
+ mNeedsReset = false;
+ mScroller.start();
+ }
+
+ final ClampedScroller scroller = mScroller;
+ if (scroller.isFinished() || !shouldAnimate()) {
+ mAnimating = false;
+ return;
+ }
+
+ if (mNeedsCancel) {
+ mNeedsCancel = false;
+ cancelTargetTouch();
+ }
+
+ scroller.computeScrollDelta();
+
+ final int deltaX = scroller.getDeltaX();
+ final int deltaY = scroller.getDeltaY();
+ scrollTargetBy(deltaX, deltaY);
+
+ // Keep going until the scroller has permanently stopped.
+ mTarget.postOnAnimation(this);
+ }
+ }
+
+ /**
+ * Scroller whose velocity follows the curve of an {@link Interpolator} and
+ * is clamped to the interpolated 0f value before starting and the
+ * interpolated 1f value after a specified duration.
+ */
+ private static class ClampedScroller {
+ private int mRampUpDuration;
+ private int mRampDownDuration;
+ private float mTargetVelocityX;
+ private float mTargetVelocityY;
+
+ private long mStartTime;
+
+ private long mDeltaTime;
+ private int mDeltaX;
+ private int mDeltaY;
+
+ private long mStopTime;
+ private float mStopValue;
+ private int mEffectiveRampDown;
+
+ /**
+ * Creates a new ramp-up scroller that reaches full velocity after a
+ * specified duration.
+ */
+ public ClampedScroller() {
+ mStartTime = Long.MIN_VALUE;
+ mStopTime = -1;
+ mDeltaTime = 0;
+ mDeltaX = 0;
+ mDeltaY = 0;
+ }
+
+ public void setRampUpDuration(int durationMillis) {
+ mRampUpDuration = durationMillis;
+ }
+
+ public void setRampDownDuration(int durationMillis) {
+ mRampDownDuration = durationMillis;
+ }
+
+ /**
+ * Starts the scroller at the current animation time.
+ */
+ public void start() {
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mStopTime = -1;
+ mDeltaTime = mStartTime;
+ mStopValue = 0.5f;
+ mDeltaX = 0;
+ mDeltaY = 0;
+ }
+
+ /**
+ * Stops the scroller at the current animation time.
+ */
+ public void requestStop() {
+ final long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration);
+ mStopValue = getValueAt(currentTime);
+ mStopTime = currentTime;
+ }
+
+ public boolean isFinished() {
+ return mStopTime > 0
+ && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
+ }
+
+ private float getValueAt(long currentTime) {
+ if (currentTime < mStartTime) {
+ return 0f;
+ } else if (mStopTime < 0 || currentTime < mStopTime) {
+ final long elapsedSinceStart = currentTime - mStartTime;
+ return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1);
+ } else {
+ final long elapsedSinceEnd = currentTime - mStopTime;
+ return (1 - mStopValue) + mStopValue
+ * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1);
+ }
+ }
+
+ /**
+ * Interpolates the value along a parabolic curve corresponding to the equation
+ * <code>y = -4x * (x-1)</code>.
+ *
+ * @param value The value to interpolate, between 0 and 1.
+ * @return the interpolated value, between 0 and 1.
+ */
+ private float interpolateValue(float value) {
+ return -4 * value * value + 4 * value;
+ }
+
+ /**
+ * Computes the current scroll deltas. This usually only be called after
+ * starting the scroller with {@link #start()}.
+ *
+ * @see #getDeltaX()
+ * @see #getDeltaY()
+ */
+ public void computeScrollDelta() {
+ if (mDeltaTime == 0) {
+ throw new RuntimeException("Cannot compute scroll delta before calling start()");
+ }
+
+ final long currentTime = AnimationUtils.currentAnimationTimeMillis();
+ final float value = getValueAt(currentTime);
+ final float scale = interpolateValue(value);
+ final long elapsedSinceDelta = currentTime - mDeltaTime;
+
+ mDeltaTime = currentTime;
+ mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
+ mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
+ }
+
+ /**
+ * Sets the target velocity for this scroller.
+ *
+ * @param x The target X velocity in pixels per millisecond.
+ * @param y The target Y velocity in pixels per millisecond.
+ */
+ public void setTargetVelocity(float x, float y) {
+ mTargetVelocityX = x;
+ mTargetVelocityY = y;
+ }
+
+ public int getHorizontalDirection() {
+ return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
+ }
+
+ public int getVerticalDirection() {
+ return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY));
+ }
+
+ /**
+ * The distance traveled in the X-coordinate computed by the last call
+ * to {@link #computeScrollDelta()}.
+ */
+ public int getDeltaX() {
+ return mDeltaX;
+ }
+
+ /**
+ * The distance traveled in the Y-coordinate computed by the last call
+ * to {@link #computeScrollDelta()}.
+ */
+ public int getDeltaY() {
+ return mDeltaY;
+ }
+ }
+
+ /**
+ * An implementation of {@link AutoScrollHelper} that knows how to scroll
+ * through an {@link AbsListView}.
+ */
+ public static class AbsListViewAutoScroller extends AutoScrollHelper {
+ private final AbsListView mTarget;
+
+ public AbsListViewAutoScroller(AbsListView target) {
+ super(target);
+
+ mTarget = target;
+ }
+
+ @Override
+ public void scrollTargetBy(int deltaX, int deltaY) {
+ mTarget.scrollListBy(deltaY);
+ }
+
+ @Override
+ public boolean canTargetScrollHorizontally(int direction) {
+ // List do not scroll horizontally.
+ return false;
+ }
+
+ @Override
+ public boolean canTargetScrollVertically(int direction) {
+ final AbsListView target = mTarget;
+ final int itemCount = target.getCount();
+ if (itemCount == 0) {
+ return false;
+ }
+
+ final int childCount = target.getChildCount();
+ final int firstPosition = target.getFirstVisiblePosition();
+ final int lastPosition = firstPosition + childCount;
+
+ if (direction > 0) {
+ // Are we already showing the entire last item?
+ if (lastPosition >= itemCount) {
+ final View lastView = target.getChildAt(childCount - 1);
+ if (lastView.getBottom() <= target.getHeight()) {
+ return false;
+ }
+ }
+ } else if (direction < 0) {
+ // Are we already showing the entire first item?
+ if (firstPosition <= 0) {
+ final View firstView = target.getChildAt(0);
+ if (firstView.getTop() >= 0) {
+ return false;
+ }
+ }
+ } else {
+ // The behavior for direction 0 is undefined and we can return
+ // whatever we want.
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/com/android/internal/widget/BackgroundFallback.java b/com/android/internal/widget/BackgroundFallback.java
new file mode 100644
index 0000000..a66fa65
--- /dev/null
+++ b/com/android/internal/widget/BackgroundFallback.java
@@ -0,0 +1,182 @@
+/*
+ * 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Helper class for drawing a fallback background in framework decor layouts.
+ * Useful for when an app has not set a window background but we're asked to draw
+ * an uncovered area.
+ */
+public class BackgroundFallback {
+ private Drawable mBackgroundFallback;
+
+ public void setDrawable(Drawable d) {
+ mBackgroundFallback = d;
+ }
+
+ public @Nullable Drawable getDrawable() {
+ return mBackgroundFallback;
+ }
+
+ public boolean hasFallback() {
+ return mBackgroundFallback != null;
+ }
+
+ /**
+ * Draws the fallback background.
+ *
+ * @param boundsView The view determining with which bounds the background should be drawn.
+ * @param root The view group containing the content.
+ * @param c The canvas to draw the background onto.
+ * @param content The view where the actual app content is contained in.
+ * @param coveringView1 A potentially opaque view drawn atop the content
+ * @param coveringView2 A potentially opaque view drawn atop the content
+ */
+ public void draw(ViewGroup boundsView, ViewGroup root, Canvas c, View content,
+ View coveringView1, View coveringView2) {
+ if (!hasFallback()) {
+ return;
+ }
+
+ // Draw the fallback in the padding.
+ final int width = boundsView.getWidth();
+ final int height = boundsView.getHeight();
+
+ final int rootOffsetX = root.getLeft();
+ final int rootOffsetY = root.getTop();
+
+ int left = width;
+ int top = height;
+ int right = 0;
+ int bottom = 0;
+
+ final int childCount = root.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = root.getChildAt(i);
+ final Drawable childBg = child.getBackground();
+ if (child == content) {
+ // We always count the content view container unless it has no background
+ // and no children.
+ if (childBg == null && child instanceof ViewGroup &&
+ ((ViewGroup) child).getChildCount() == 0) {
+ continue;
+ }
+ } else if (child.getVisibility() != View.VISIBLE || !isOpaque(childBg)) {
+ // Potentially translucent or invisible children don't count, and we assume
+ // the content view will cover the whole area if we're in a background
+ // fallback situation.
+ continue;
+ }
+ left = Math.min(left, rootOffsetX + child.getLeft());
+ top = Math.min(top, rootOffsetY + child.getTop());
+ right = Math.max(right, rootOffsetX + child.getRight());
+ bottom = Math.max(bottom, rootOffsetY + child.getBottom());
+ }
+
+ // If one of the bar backgrounds is a solid color and covers the entire padding on a side
+ // we can drop that padding.
+ boolean eachBarCoversTopInY = true;
+ for (int i = 0; i < 2; i++) {
+ View v = (i == 0) ? coveringView1 : coveringView2;
+ if (v == null || v.getVisibility() != View.VISIBLE
+ || v.getAlpha() != 1f || !isOpaque(v.getBackground())) {
+ eachBarCoversTopInY = false;
+ continue;
+ }
+
+ // Bar covers entire left padding
+ if (v.getTop() <= 0 && v.getBottom() >= height
+ && v.getLeft() <= 0 && v.getRight() >= left) {
+ left = 0;
+ }
+ // Bar covers entire right padding
+ if (v.getTop() <= 0 && v.getBottom() >= height
+ && v.getLeft() <= right && v.getRight() >= width) {
+ right = width;
+ }
+ // Bar covers entire top padding
+ if (v.getTop() <= 0 && v.getBottom() >= top
+ && v.getLeft() <= 0 && v.getRight() >= width) {
+ top = 0;
+ }
+ // Bar covers entire bottom padding
+ if (v.getTop() <= bottom && v.getBottom() >= height
+ && v.getLeft() <= 0 && v.getRight() >= width) {
+ bottom = height;
+ }
+
+ eachBarCoversTopInY &= v.getTop() <= 0 && v.getBottom() >= top;
+ }
+
+ // Special case: Sometimes, both covering views together may cover the top inset, but
+ // neither does on its own.
+ if (eachBarCoversTopInY && (viewsCoverEntireWidth(coveringView1, coveringView2, width)
+ || viewsCoverEntireWidth(coveringView2, coveringView1, width))) {
+ top = 0;
+ }
+
+ if (left >= right || top >= bottom) {
+ // No valid area to draw in.
+ return;
+ }
+
+ if (top > 0) {
+ mBackgroundFallback.setBounds(0, 0, width, top);
+ mBackgroundFallback.draw(c);
+ }
+ if (left > 0) {
+ mBackgroundFallback.setBounds(0, top, left, height);
+ mBackgroundFallback.draw(c);
+ }
+ if (right < width) {
+ mBackgroundFallback.setBounds(right, top, width, height);
+ mBackgroundFallback.draw(c);
+ }
+ if (bottom < height) {
+ mBackgroundFallback.setBounds(left, bottom, right, height);
+ mBackgroundFallback.draw(c);
+ }
+ }
+
+ private boolean isOpaque(Drawable childBg) {
+ return childBg != null && childBg.getOpacity() == PixelFormat.OPAQUE;
+ }
+
+ /**
+ * Returns true if {@code view1} starts before or on {@code 0} and extends at least
+ * up to {@code view2}, and that view extends at least to {@code width}.
+ *
+ * @param view1 the first view to check if it covers the width
+ * @param view2 the second view to check if it covers the width
+ * @param width the width to check for
+ * @return returns true if both views together cover the entire width (and view1 is to the left
+ * of view2)
+ */
+ private boolean viewsCoverEntireWidth(View view1, View view2, int width) {
+ return view1.getLeft() <= 0
+ && view1.getRight() >= view2.getLeft()
+ && view2.getRight() >= width;
+ }
+}
diff --git a/com/android/internal/widget/ButtonBarLayout.java b/com/android/internal/widget/ButtonBarLayout.java
new file mode 100644
index 0000000..ff13107
--- /dev/null
+++ b/com/android/internal/widget/ButtonBarLayout.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.internal.R;
+
+/**
+ * An extension of LinearLayout that automatically switches to vertical
+ * orientation when it can't fit its child views horizontally.
+ */
+public class ButtonBarLayout extends LinearLayout {
+ /** Amount of the second button to "peek" above the fold when stacked. */
+ private static final int PEEK_BUTTON_DP = 16;
+
+ /** Whether the current configuration allows stacking. */
+ private boolean mAllowStacking;
+
+ private int mLastWidthSize = -1;
+
+ private int mMinimumHeight = 0;
+
+ @UnsupportedAppUsage
+ public ButtonBarLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout);
+ mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true);
+ ta.recycle();
+ }
+
+ public void setAllowStacking(boolean allowStacking) {
+ if (mAllowStacking != allowStacking) {
+ mAllowStacking = allowStacking;
+ if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) {
+ setStacked(false);
+ }
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ if (mAllowStacking) {
+ if (widthSize > mLastWidthSize && isStacked()) {
+ // We're being measured wider this time, try un-stacking.
+ setStacked(false);
+ }
+
+ mLastWidthSize = widthSize;
+ }
+
+ boolean needsRemeasure = false;
+
+ // If we're not stacked, make sure the measure spec is AT_MOST rather
+ // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
+ // know to stack the buttons.
+ final int initialWidthMeasureSpec;
+ if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
+
+ // We'll need to remeasure again to fill excess space.
+ needsRemeasure = true;
+ } else {
+ initialWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
+
+ if (mAllowStacking && !isStacked()) {
+ final int measuredWidth = getMeasuredWidthAndState();
+ final int measuredWidthState = measuredWidth & MEASURED_STATE_MASK;
+ if (measuredWidthState == MEASURED_STATE_TOO_SMALL) {
+ setStacked(true);
+
+ // Measure again in the new orientation.
+ needsRemeasure = true;
+ }
+ }
+
+ if (needsRemeasure) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ // Compute minimum height such that, when stacked, some portion of the
+ // second button is visible.
+ int minHeight = 0;
+ final int firstVisible = getNextVisibleChildIndex(0);
+ if (firstVisible >= 0) {
+ final View firstButton = getChildAt(firstVisible);
+ final LayoutParams firstParams = (LayoutParams) firstButton.getLayoutParams();
+ minHeight += getPaddingTop() + firstButton.getMeasuredHeight()
+ + firstParams.topMargin + firstParams.bottomMargin;
+ if (isStacked()) {
+ final int secondVisible = getNextVisibleChildIndex(firstVisible + 1);
+ if (secondVisible >= 0) {
+ minHeight += getChildAt(secondVisible).getPaddingTop()
+ + PEEK_BUTTON_DP * getResources().getDisplayMetrics().density;
+ }
+ } else {
+ minHeight += getPaddingBottom();
+ }
+ }
+
+ if (getMinimumHeight() != minHeight) {
+ setMinimumHeight(minHeight);
+ }
+ }
+
+ private int getNextVisibleChildIndex(int index) {
+ for (int i = index, count = getChildCount(); i < count; i++) {
+ if (getChildAt(i).getVisibility() == View.VISIBLE) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ return Math.max(mMinimumHeight, super.getMinimumHeight());
+ }
+
+ private void setStacked(boolean stacked) {
+ setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
+ setGravity(stacked ? Gravity.END : Gravity.BOTTOM);
+
+ final View spacer = findViewById(R.id.spacer);
+ if (spacer != null) {
+ spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE);
+ }
+
+ // Reverse the child order. This is specific to the Material button
+ // bar's layout XML and will probably not generalize.
+ final int childCount = getChildCount();
+ for (int i = childCount - 2; i >= 0; i--) {
+ bringChildToFront(getChildAt(i));
+ }
+ }
+
+ private boolean isStacked() {
+ return getOrientation() == LinearLayout.VERTICAL;
+ }
+}
diff --git a/com/android/internal/widget/CachingIconView.java b/com/android/internal/widget/CachingIconView.java
new file mode 100644
index 0000000..84cde1b
--- /dev/null
+++ b/com/android/internal/widget/CachingIconView.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * An ImageView for displaying an Icon. Avoids reloading the Icon when possible.
+ */
[email protected]
+public class CachingIconView extends ImageView {
+
+ private String mLastPackage;
+ private int mLastResId;
+ private boolean mInternalSetDrawable;
+ private boolean mForceHidden;
+ private int mDesiredVisibility;
+ private Consumer<Integer> mOnVisibilityChangedListener;
+ private Consumer<Boolean> mOnForceHiddenChangedListener;
+ private int mIconColor;
+ private boolean mWillBeForceHidden;
+
+ @UnsupportedAppUsage
+ public CachingIconView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ @RemotableViewMethod(asyncImpl="setImageIconAsync")
+ public void setImageIcon(@Nullable Icon icon) {
+ if (!testAndSetCache(icon)) {
+ mInternalSetDrawable = true;
+ // This calls back to setImageDrawable, make sure we don't clear the cache there.
+ super.setImageIcon(icon);
+ mInternalSetDrawable = false;
+ }
+ }
+
+ @Override
+ public Runnable setImageIconAsync(@Nullable Icon icon) {
+ resetCache();
+ return super.setImageIconAsync(icon);
+ }
+
+ @Override
+ @RemotableViewMethod(asyncImpl="setImageResourceAsync")
+ public void setImageResource(@DrawableRes int resId) {
+ if (!testAndSetCache(resId)) {
+ mInternalSetDrawable = true;
+ // This calls back to setImageDrawable, make sure we don't clear the cache there.
+ super.setImageResource(resId);
+ mInternalSetDrawable = false;
+ }
+ }
+
+ @Override
+ public Runnable setImageResourceAsync(@DrawableRes int resId) {
+ resetCache();
+ return super.setImageResourceAsync(resId);
+ }
+
+ @Override
+ @RemotableViewMethod(asyncImpl="setImageURIAsync")
+ public void setImageURI(@Nullable Uri uri) {
+ resetCache();
+ super.setImageURI(uri);
+ }
+
+ @Override
+ public Runnable setImageURIAsync(@Nullable Uri uri) {
+ resetCache();
+ return super.setImageURIAsync(uri);
+ }
+
+ @Override
+ public void setImageDrawable(@Nullable Drawable drawable) {
+ if (!mInternalSetDrawable) {
+ // Only clear the cache if we were externally called.
+ resetCache();
+ }
+ super.setImageDrawable(drawable);
+ }
+
+ @Override
+ @RemotableViewMethod
+ public void setImageBitmap(Bitmap bm) {
+ resetCache();
+ super.setImageBitmap(bm);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ resetCache();
+ }
+
+ /**
+ * @return true if the currently set image is the same as {@param icon}
+ */
+ private synchronized boolean testAndSetCache(Icon icon) {
+ if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
+ String iconPackage = normalizeIconPackage(icon);
+
+ boolean isCached = mLastResId != 0
+ && icon.getResId() == mLastResId
+ && Objects.equals(iconPackage, mLastPackage);
+
+ mLastPackage = iconPackage;
+ mLastResId = icon.getResId();
+
+ return isCached;
+ } else {
+ resetCache();
+ return false;
+ }
+ }
+
+ /**
+ * @return true if the currently set image is the same as {@param resId}
+ */
+ private synchronized boolean testAndSetCache(int resId) {
+ boolean isCached;
+ if (resId == 0 || mLastResId == 0) {
+ isCached = false;
+ } else {
+ isCached = resId == mLastResId && null == mLastPackage;
+ }
+ mLastPackage = null;
+ mLastResId = resId;
+ return isCached;
+ }
+
+ /**
+ * Returns the normalized package name of {@param icon}.
+ * @return null if icon is null or if the icons package is null, empty or matches the current
+ * context. Otherwise returns the icon's package context.
+ */
+ private String normalizeIconPackage(Icon icon) {
+ if (icon == null) {
+ return null;
+ }
+
+ String pkg = icon.getResPackage();
+ if (TextUtils.isEmpty(pkg)) {
+ return null;
+ }
+ if (pkg.equals(mContext.getPackageName())) {
+ return null;
+ }
+ return pkg;
+ }
+
+ private synchronized void resetCache() {
+ mLastResId = 0;
+ mLastPackage = null;
+ }
+
+ /**
+ * Set the icon to be forcibly hidden, even when it's visibility is changed to visible.
+ * This is necessary since we still want to keep certain views hidden when their visibility
+ * is modified from other sources like the shelf.
+ */
+ public void setForceHidden(boolean forceHidden) {
+ if (forceHidden != mForceHidden) {
+ mForceHidden = forceHidden;
+ mWillBeForceHidden = false;
+ updateVisibility();
+ if (mOnForceHiddenChangedListener != null) {
+ mOnForceHiddenChangedListener.accept(forceHidden);
+ }
+ }
+ }
+
+ @Override
+ @RemotableViewMethod
+ public void setVisibility(int visibility) {
+ mDesiredVisibility = visibility;
+ updateVisibility();
+ }
+
+ private void updateVisibility() {
+ int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE
+ : mDesiredVisibility;
+ if (mOnVisibilityChangedListener != null) {
+ mOnVisibilityChangedListener.accept(visibility);
+ }
+ super.setVisibility(visibility);
+ }
+
+ public void setOnVisibilityChangedListener(Consumer<Integer> listener) {
+ mOnVisibilityChangedListener = listener;
+ }
+
+ public void setOnForceHiddenChangedListener(Consumer<Boolean> listener) {
+ mOnForceHiddenChangedListener = listener;
+ }
+
+
+ public boolean isForceHidden() {
+ return mForceHidden;
+ }
+
+ @RemotableViewMethod
+ public void setOriginalIconColor(int color) {
+ mIconColor = color;
+ }
+
+ public int getOriginalIconColor() {
+ return mIconColor;
+ }
+
+ /**
+ * @return if the view will be forceHidden after an animation
+ */
+ public boolean willBeForceHidden() {
+ return mWillBeForceHidden;
+ }
+
+ /**
+ * Set that this view will be force hidden after an animation
+ *
+ * @param forceHidden if it will be forcehidden
+ */
+ public void setWillBeForceHidden(boolean forceHidden) {
+ mWillBeForceHidden = forceHidden;
+ }
+}
diff --git a/com/android/internal/widget/ChildHelper.java b/com/android/internal/widget/ChildHelper.java
new file mode 100644
index 0000000..e9136d0
--- /dev/null
+++ b/com/android/internal/widget/ChildHelper.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to manage children.
+ * <p>
+ * It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
+ * provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
+ * like getChildAt, getChildCount etc. These methods ignore hidden children.
+ * <p>
+ * When RecyclerView needs direct access to the view group children, it can call unfiltered
+ * methods like get getUnfilteredChildCount or getUnfilteredChildAt.
+ */
+class ChildHelper {
+
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "ChildrenHelper";
+
+ final Callback mCallback;
+
+ final Bucket mBucket;
+
+ final List<View> mHiddenViews;
+
+ ChildHelper(Callback callback) {
+ mCallback = callback;
+ mBucket = new Bucket();
+ mHiddenViews = new ArrayList<View>();
+ }
+
+ /**
+ * Marks a child view as hidden
+ *
+ * @param child View to hide.
+ */
+ private void hideViewInternal(View child) {
+ mHiddenViews.add(child);
+ mCallback.onEnteredHiddenState(child);
+ }
+
+ /**
+ * Unmarks a child view as hidden.
+ *
+ * @param child View to hide.
+ */
+ private boolean unhideViewInternal(View child) {
+ if (mHiddenViews.remove(child)) {
+ mCallback.onLeftHiddenState(child);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Adds a view to the ViewGroup
+ *
+ * @param child View to add.
+ * @param hidden If set to true, this item will be invisible from regular methods.
+ */
+ void addView(View child, boolean hidden) {
+ addView(child, -1, hidden);
+ }
+
+ /**
+ * Add a view to the ViewGroup at an index
+ *
+ * @param child View to add.
+ * @param index Index of the child from the regular perspective (excluding hidden views).
+ * ChildHelper offsets this index to actual ViewGroup index.
+ * @param hidden If set to true, this item will be invisible from regular methods.
+ */
+ void addView(View child, int index, boolean hidden) {
+ final int offset;
+ if (index < 0) {
+ offset = mCallback.getChildCount();
+ } else {
+ offset = getOffset(index);
+ }
+ mBucket.insert(offset, hidden);
+ if (hidden) {
+ hideViewInternal(child);
+ }
+ mCallback.addView(child, offset);
+ if (DEBUG) {
+ Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
+ }
+ }
+
+ private int getOffset(int index) {
+ if (index < 0) {
+ return -1; //anything below 0 won't work as diff will be undefined.
+ }
+ final int limit = mCallback.getChildCount();
+ int offset = index;
+ while (offset < limit) {
+ final int removedBefore = mBucket.countOnesBefore(offset);
+ final int diff = index - (offset - removedBefore);
+ if (diff == 0) {
+ while (mBucket.get(offset)) { // ensure this offset is not hidden
+ offset++;
+ }
+ return offset;
+ } else {
+ offset += diff;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes the provided View from underlying RecyclerView.
+ *
+ * @param view The view to remove.
+ */
+ void removeView(View view) {
+ int index = mCallback.indexOfChild(view);
+ if (index < 0) {
+ return;
+ }
+ if (mBucket.remove(index)) {
+ unhideViewInternal(view);
+ }
+ mCallback.removeViewAt(index);
+ if (DEBUG) {
+ Log.d(TAG, "remove View off:" + index + "," + this);
+ }
+ }
+
+ /**
+ * Removes the view at the provided index from RecyclerView.
+ *
+ * @param index Index of the child from the regular perspective (excluding hidden views).
+ * ChildHelper offsets this index to actual ViewGroup index.
+ */
+ void removeViewAt(int index) {
+ final int offset = getOffset(index);
+ final View view = mCallback.getChildAt(offset);
+ if (view == null) {
+ return;
+ }
+ if (mBucket.remove(offset)) {
+ unhideViewInternal(view);
+ }
+ mCallback.removeViewAt(offset);
+ if (DEBUG) {
+ Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
+ }
+ }
+
+ /**
+ * Returns the child at provided index.
+ *
+ * @param index Index of the child to return in regular perspective.
+ */
+ View getChildAt(int index) {
+ final int offset = getOffset(index);
+ return mCallback.getChildAt(offset);
+ }
+
+ /**
+ * Removes all views from the ViewGroup including the hidden ones.
+ */
+ void removeAllViewsUnfiltered() {
+ mBucket.reset();
+ for (int i = mHiddenViews.size() - 1; i >= 0; i--) {
+ mCallback.onLeftHiddenState(mHiddenViews.get(i));
+ mHiddenViews.remove(i);
+ }
+ mCallback.removeAllViews();
+ if (DEBUG) {
+ Log.d(TAG, "removeAllViewsUnfiltered");
+ }
+ }
+
+ /**
+ * This can be used to find a disappearing view by position.
+ *
+ * @param position The adapter position of the item.
+ * @return A hidden view with a valid ViewHolder that matches the position.
+ */
+ View findHiddenNonRemovedView(int position) {
+ final int count = mHiddenViews.size();
+ for (int i = 0; i < count; i++) {
+ final View view = mHiddenViews.get(i);
+ RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
+ if (holder.getLayoutPosition() == position
+ && !holder.isInvalid()
+ && !holder.isRemoved()) {
+ return view;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Attaches the provided view to the underlying ViewGroup.
+ *
+ * @param child Child to attach.
+ * @param index Index of the child to attach in regular perspective.
+ * @param layoutParams LayoutParams for the child.
+ * @param hidden If set to true, this item will be invisible to the regular methods.
+ */
+ void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
+ boolean hidden) {
+ final int offset;
+ if (index < 0) {
+ offset = mCallback.getChildCount();
+ } else {
+ offset = getOffset(index);
+ }
+ mBucket.insert(offset, hidden);
+ if (hidden) {
+ hideViewInternal(child);
+ }
+ mCallback.attachViewToParent(child, offset, layoutParams);
+ if (DEBUG) {
+ Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + ","
+ + "h:" + hidden + ", " + this);
+ }
+ }
+
+ /**
+ * Returns the number of children that are not hidden.
+ *
+ * @return Number of children that are not hidden.
+ * @see #getChildAt(int)
+ */
+ int getChildCount() {
+ return mCallback.getChildCount() - mHiddenViews.size();
+ }
+
+ /**
+ * Returns the total number of children.
+ *
+ * @return The total number of children including the hidden views.
+ * @see #getUnfilteredChildAt(int)
+ */
+ int getUnfilteredChildCount() {
+ return mCallback.getChildCount();
+ }
+
+ /**
+ * Returns a child by ViewGroup offset. ChildHelper won't offset this index.
+ *
+ * @param index ViewGroup index of the child to return.
+ * @return The view in the provided index.
+ */
+ View getUnfilteredChildAt(int index) {
+ return mCallback.getChildAt(index);
+ }
+
+ /**
+ * Detaches the view at the provided index.
+ *
+ * @param index Index of the child to return in regular perspective.
+ */
+ void detachViewFromParent(int index) {
+ final int offset = getOffset(index);
+ mBucket.remove(offset);
+ mCallback.detachViewFromParent(offset);
+ if (DEBUG) {
+ Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
+ }
+ }
+
+ /**
+ * Returns the index of the child in regular perspective.
+ *
+ * @param child The child whose index will be returned.
+ * @return The regular perspective index of the child or -1 if it does not exists.
+ */
+ int indexOfChild(View child) {
+ final int index = mCallback.indexOfChild(child);
+ if (index == -1) {
+ return -1;
+ }
+ if (mBucket.get(index)) {
+ if (DEBUG) {
+ throw new IllegalArgumentException("cannot get index of a hidden child");
+ } else {
+ return -1;
+ }
+ }
+ // reverse the index
+ return index - mBucket.countOnesBefore(index);
+ }
+
+ /**
+ * Returns whether a View is visible to LayoutManager or not.
+ *
+ * @param view The child view to check. Should be a child of the Callback.
+ * @return True if the View is not visible to LayoutManager
+ */
+ boolean isHidden(View view) {
+ return mHiddenViews.contains(view);
+ }
+
+ /**
+ * Marks a child view as hidden.
+ *
+ * @param view The view to hide.
+ */
+ void hide(View view) {
+ final int offset = mCallback.indexOfChild(view);
+ if (offset < 0) {
+ throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+ }
+ if (DEBUG && mBucket.get(offset)) {
+ throw new RuntimeException("trying to hide same view twice, how come ? " + view);
+ }
+ mBucket.set(offset);
+ hideViewInternal(view);
+ if (DEBUG) {
+ Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this);
+ }
+ }
+
+ /**
+ * Moves a child view from hidden list to regular list.
+ * Calling this method should probably be followed by a detach, otherwise, it will suddenly
+ * show up in LayoutManager's children list.
+ *
+ * @param view The hidden View to unhide
+ */
+ void unhide(View view) {
+ final int offset = mCallback.indexOfChild(view);
+ if (offset < 0) {
+ throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+ }
+ if (!mBucket.get(offset)) {
+ throw new RuntimeException("trying to unhide a view that was not hidden" + view);
+ }
+ mBucket.clear(offset);
+ unhideViewInternal(view);
+ }
+
+ @Override
+ public String toString() {
+ return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
+ }
+
+ /**
+ * Removes a view from the ViewGroup if it is hidden.
+ *
+ * @param view The view to remove.
+ * @return True if the View is found and it is hidden. False otherwise.
+ */
+ boolean removeViewIfHidden(View view) {
+ final int index = mCallback.indexOfChild(view);
+ if (index == -1) {
+ if (unhideViewInternal(view) && DEBUG) {
+ throw new IllegalStateException("view is in hidden list but not in view group");
+ }
+ return true;
+ }
+ if (mBucket.get(index)) {
+ mBucket.remove(index);
+ if (!unhideViewInternal(view) && DEBUG) {
+ throw new IllegalStateException(
+ "removed a hidden view but it is not in hidden views list");
+ }
+ mCallback.removeViewAt(index);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Bitset implementation that provides methods to offset indices.
+ */
+ static class Bucket {
+
+ static final int BITS_PER_WORD = Long.SIZE;
+
+ static final long LAST_BIT = 1L << (Long.SIZE - 1);
+
+ long mData = 0;
+
+ Bucket mNext;
+
+ void set(int index) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ mNext.set(index - BITS_PER_WORD);
+ } else {
+ mData |= 1L << index;
+ }
+ }
+
+ private void ensureNext() {
+ if (mNext == null) {
+ mNext = new Bucket();
+ }
+ }
+
+ void clear(int index) {
+ if (index >= BITS_PER_WORD) {
+ if (mNext != null) {
+ mNext.clear(index - BITS_PER_WORD);
+ }
+ } else {
+ mData &= ~(1L << index);
+ }
+
+ }
+
+ boolean get(int index) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ return mNext.get(index - BITS_PER_WORD);
+ } else {
+ return (mData & (1L << index)) != 0;
+ }
+ }
+
+ void reset() {
+ mData = 0;
+ if (mNext != null) {
+ mNext.reset();
+ }
+ }
+
+ void insert(int index, boolean value) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ mNext.insert(index - BITS_PER_WORD, value);
+ } else {
+ final boolean lastBit = (mData & LAST_BIT) != 0;
+ long mask = (1L << index) - 1;
+ final long before = mData & mask;
+ final long after = ((mData & ~mask)) << 1;
+ mData = before | after;
+ if (value) {
+ set(index);
+ } else {
+ clear(index);
+ }
+ if (lastBit || mNext != null) {
+ ensureNext();
+ mNext.insert(0, lastBit);
+ }
+ }
+ }
+
+ boolean remove(int index) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ return mNext.remove(index - BITS_PER_WORD);
+ } else {
+ long mask = (1L << index);
+ final boolean value = (mData & mask) != 0;
+ mData &= ~mask;
+ mask = mask - 1;
+ final long before = mData & mask;
+ // cannot use >> because it adds one.
+ final long after = Long.rotateRight(mData & ~mask, 1);
+ mData = before | after;
+ if (mNext != null) {
+ if (mNext.get(0)) {
+ set(BITS_PER_WORD - 1);
+ }
+ mNext.remove(0);
+ }
+ return value;
+ }
+ }
+
+ int countOnesBefore(int index) {
+ if (mNext == null) {
+ if (index >= BITS_PER_WORD) {
+ return Long.bitCount(mData);
+ }
+ return Long.bitCount(mData & ((1L << index) - 1));
+ }
+ if (index < BITS_PER_WORD) {
+ return Long.bitCount(mData & ((1L << index) - 1));
+ } else {
+ return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mNext == null ? Long.toBinaryString(mData)
+ : mNext.toString() + "xx" + Long.toBinaryString(mData);
+ }
+ }
+
+ interface Callback {
+
+ int getChildCount();
+
+ void addView(View child, int index);
+
+ int indexOfChild(View view);
+
+ void removeViewAt(int index);
+
+ View getChildAt(int offset);
+
+ void removeAllViews();
+
+ RecyclerView.ViewHolder getChildViewHolder(View view);
+
+ void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
+
+ void detachViewFromParent(int offset);
+
+ void onEnteredHiddenState(View child);
+
+ void onLeftHiddenState(View child);
+ }
+}
+
diff --git a/com/android/internal/widget/ConversationLayout.java b/com/android/internal/widget/ConversationLayout.java
new file mode 100644
index 0000000..688e00b
--- /dev/null
+++ b/com/android/internal/widget/ConversationLayout.java
@@ -0,0 +1,1268 @@
+/*
+ * Copyright (C) 2020 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.internal.widget;
+
+import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL;
+import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE;
+import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_IN;
+import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.app.Person;
+import android.app.RemoteInputHistoryItem;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.util.ContrastColorUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+
+/**
+ * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
+ * messages and adapts the layout accordingly.
+ */
[email protected]
+public class ConversationLayout extends FrameLayout
+ implements ImageMessageConsumer, IMessagingLayout {
+
+ private static final float COLOR_SHIFT_AMOUNT = 60;
+ /**
+ * Pattren for filter some ingonable characters.
+ * p{Z} for any kind of whitespace or invisible separator.
+ * p{C} for any kind of punctuation character.
+ */
+ private static final Pattern IGNORABLE_CHAR_PATTERN
+ = Pattern.compile("[\\p{C}\\p{Z}]");
+ private static final Pattern SPECIAL_CHAR_PATTERN
+ = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
+ private static final Consumer<MessagingMessage> REMOVE_MESSAGE
+ = MessagingMessage::removeMessage;
+ public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+ public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+ public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+ public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
+ = new MessagingPropertyAnimator();
+ private List<MessagingMessage> mMessages = new ArrayList<>();
+ private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
+ private MessagingLinearLayout mMessagingLinearLayout;
+ private boolean mShowHistoricMessages;
+ private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
+ private int mLayoutColor;
+ private int mSenderTextColor;
+ private int mMessageTextColor;
+ private int mAvatarSize;
+ private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private Paint mTextPaint = new Paint();
+ private Icon mAvatarReplacement;
+ private boolean mIsOneToOne;
+ private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
+ private Person mUser;
+ private CharSequence mNameReplacement;
+ private boolean mIsCollapsed;
+ private ImageResolver mImageResolver;
+ private CachingIconView mConversationIconView;
+ private View mConversationIconContainer;
+ private int mConversationIconTopPaddingExpandedGroup;
+ private int mConversationIconTopPadding;
+ private int mExpandedGroupMessagePadding;
+ private TextView mConversationText;
+ private View mConversationIconBadge;
+ private CachingIconView mConversationIconBadgeBg;
+ private Icon mLargeIcon;
+ private View mExpandButtonContainer;
+ private View mExpandButtonInnerContainer;
+ private ViewGroup mExpandButtonAndContentContainer;
+ private NotificationExpandButton mExpandButton;
+ private MessagingLinearLayout mImageMessageContainer;
+ private int mExpandButtonExpandedTopMargin;
+ private int mBadgedSideMargins;
+ private int mConversationAvatarSize;
+ private int mConversationAvatarSizeExpanded;
+ private CachingIconView mIcon;
+ private CachingIconView mImportanceRingView;
+ private int mExpandedGroupSideMargin;
+ private int mExpandedGroupSideMarginFacePile;
+ private View mConversationFacePile;
+ private int mNotificationBackgroundColor;
+ private CharSequence mFallbackChatName;
+ private CharSequence mFallbackGroupChatName;
+ private CharSequence mConversationTitle;
+ private int mNotificationHeaderExpandedPadding;
+ private View mConversationHeader;
+ private View mContentContainer;
+ private boolean mExpandable = true;
+ private int mContentMarginEnd;
+ private Rect mMessagingClipRect;
+ private ObservableTextView mAppName;
+ private ViewGroup mActions;
+ private int mConversationContentStart;
+ private int mInternalButtonPadding;
+ private boolean mAppNameGone;
+ private int mFacePileAvatarSize;
+ private int mFacePileAvatarSizeExpandedGroup;
+ private int mFacePileProtectionWidth;
+ private int mFacePileProtectionWidthExpanded;
+ private boolean mImportantConversation;
+ private TextView mUnreadBadge;
+ private ViewGroup mAppOps;
+ private Rect mAppOpsTouchRect = new Rect();
+ private float mMinTouchSize;
+ private Icon mConversationIcon;
+ private View mAppNameDivider;
+
+ public ConversationLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mMessagingLinearLayout = findViewById(R.id.notification_messaging);
+ mActions = findViewById(R.id.actions);
+ mMessagingLinearLayout.setMessagingLayout(this);
+ mImageMessageContainer = findViewById(R.id.conversation_image_message_container);
+ // We still want to clip, but only on the top, since views can temporarily out of bounds
+ // during transitions.
+ DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
+ mMessagingClipRect = new Rect(0, 0, size, size);
+ setMessagingClippingDisabled(false);
+ mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+ mTextPaint.setAntiAlias(true);
+ mConversationIconView = findViewById(R.id.conversation_icon);
+ mConversationIconContainer = findViewById(R.id.conversation_icon_container);
+ mIcon = findViewById(R.id.icon);
+ mAppOps = findViewById(com.android.internal.R.id.app_ops);
+ mMinTouchSize = 48 * getResources().getDisplayMetrics().density;
+ mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring);
+ mConversationIconBadge = findViewById(R.id.conversation_icon_badge);
+ mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg);
+ mIcon.setOnVisibilityChangedListener((visibility) -> {
+
+ // Let's hide the background directly or in an animated way
+ boolean isGone = visibility == GONE;
+ int oldVisibility = mConversationIconBadgeBg.getVisibility();
+ boolean wasGone = oldVisibility == GONE;
+ if (wasGone != isGone) {
+ // Keep the badge gone state in sync with the icon. This is necessary in cases
+ // Where the icon is being hidden externally like in group children.
+ mConversationIconBadgeBg.animate().cancel();
+ mConversationIconBadgeBg.setVisibility(visibility);
+ }
+
+ // Let's handle the importance ring which can also be be gone normally
+ oldVisibility = mImportanceRingView.getVisibility();
+ wasGone = oldVisibility == GONE;
+ visibility = !mImportantConversation ? GONE : visibility;
+ isGone = visibility == GONE;
+ if (wasGone != isGone) {
+ // Keep the badge visibility in sync with the icon. This is necessary in cases
+ // Where the icon is being hidden externally like in group children.
+ mImportanceRingView.animate().cancel();
+ mImportanceRingView.setVisibility(visibility);
+ }
+ });
+ // When the small icon is gone, hide the rest of the badge
+ mIcon.setOnForceHiddenChangedListener((forceHidden) -> {
+ animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
+ animateViewForceHidden(mImportanceRingView, forceHidden);
+ });
+
+ // When the conversation icon is gone, hide the whole badge
+ mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> {
+ animateViewForceHidden(mConversationIconBadgeBg, forceHidden);
+ animateViewForceHidden(mImportanceRingView, forceHidden);
+ animateViewForceHidden(mIcon, forceHidden);
+ });
+ mConversationText = findViewById(R.id.conversation_text);
+ mExpandButtonContainer = findViewById(R.id.expand_button_container);
+ mConversationHeader = findViewById(R.id.conversation_header);
+ mContentContainer = findViewById(R.id.notification_action_list_margin_target);
+ mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container);
+ mExpandButtonInnerContainer = findViewById(R.id.expand_button_inner_container);
+ mExpandButton = findViewById(R.id.expand_button);
+ mExpandButtonExpandedTopMargin = getResources().getDimensionPixelSize(
+ R.dimen.conversation_expand_button_top_margin_expanded);
+ mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize(
+ R.dimen.conversation_header_expanded_padding_end);
+ mContentMarginEnd = getResources().getDimensionPixelSize(
+ R.dimen.notification_content_margin_end);
+ mBadgedSideMargins = getResources().getDimensionPixelSize(
+ R.dimen.conversation_badge_side_margin);
+ mConversationAvatarSize = getResources().getDimensionPixelSize(
+ R.dimen.conversation_avatar_size);
+ mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize(
+ R.dimen.conversation_avatar_size_group_expanded);
+ mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize(
+ R.dimen.conversation_icon_container_top_padding_small_avatar);
+ mConversationIconTopPadding = getResources().getDimensionPixelSize(
+ R.dimen.conversation_icon_container_top_padding);
+ mExpandedGroupMessagePadding = getResources().getDimensionPixelSize(
+ R.dimen.expanded_group_conversation_message_padding);
+ mExpandedGroupSideMargin = getResources().getDimensionPixelSize(
+ R.dimen.conversation_badge_side_margin_group_expanded);
+ mExpandedGroupSideMarginFacePile = getResources().getDimensionPixelSize(
+ R.dimen.conversation_badge_side_margin_group_expanded_face_pile);
+ mConversationFacePile = findViewById(R.id.conversation_face_pile);
+ mFacePileAvatarSize = getResources().getDimensionPixelSize(
+ R.dimen.conversation_face_pile_avatar_size);
+ mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize(
+ R.dimen.conversation_face_pile_avatar_size_group_expanded);
+ mFacePileProtectionWidth = getResources().getDimensionPixelSize(
+ R.dimen.conversation_face_pile_protection_width);
+ mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize(
+ R.dimen.conversation_face_pile_protection_width_expanded);
+ mFallbackChatName = getResources().getString(
+ R.string.conversation_title_fallback_one_to_one);
+ mFallbackGroupChatName = getResources().getString(
+ R.string.conversation_title_fallback_group_chat);
+ mAppName = findViewById(R.id.app_name_text);
+ mAppNameDivider = findViewById(R.id.app_name_divider);
+ mAppNameGone = mAppName.getVisibility() == GONE;
+ mAppName.setOnVisibilityChangedListener((visibility) -> {
+ onAppNameVisibilityChanged();
+ });
+ mUnreadBadge = findViewById(R.id.conversation_unread_count);
+ mConversationContentStart = getResources().getDimensionPixelSize(
+ R.dimen.conversation_content_start);
+ mInternalButtonPadding
+ = getResources().getDimensionPixelSize(R.dimen.button_padding_horizontal_material)
+ + getResources().getDimensionPixelSize(R.dimen.button_inset_horizontal_material);
+ }
+
+ private void animateViewForceHidden(CachingIconView view, boolean forceHidden) {
+ boolean nowForceHidden = view.willBeForceHidden() || view.isForceHidden();
+ if (forceHidden == nowForceHidden) {
+ // We are either already forceHidden or will be
+ return;
+ }
+ view.animate().cancel();
+ view.setWillBeForceHidden(forceHidden);
+ view.animate()
+ .scaleX(forceHidden ? 0.5f : 1.0f)
+ .scaleY(forceHidden ? 0.5f : 1.0f)
+ .alpha(forceHidden ? 0.0f : 1.0f)
+ .setInterpolator(forceHidden ? ALPHA_OUT : ALPHA_IN)
+ .setDuration(160);
+ if (view.getVisibility() != VISIBLE) {
+ view.setForceHidden(forceHidden);
+ } else {
+ view.animate().withEndAction(() -> view.setForceHidden(forceHidden));
+ }
+ view.animate().start();
+ }
+
+ @RemotableViewMethod
+ public void setAvatarReplacement(Icon icon) {
+ mAvatarReplacement = icon;
+ }
+
+ @RemotableViewMethod
+ public void setNameReplacement(CharSequence nameReplacement) {
+ mNameReplacement = nameReplacement;
+ }
+
+ /**
+ * Sets this conversation as "important", adding some additional UI treatment.
+ */
+ @RemotableViewMethod
+ public void setIsImportantConversation(boolean isImportantConversation) {
+ mImportantConversation = isImportantConversation;
+ mImportanceRingView.setVisibility(isImportantConversation
+ && mIcon.getVisibility() != GONE ? VISIBLE : GONE);
+ }
+
+ public boolean isImportantConversation() {
+ return mImportantConversation;
+ }
+
+ /**
+ * Set this layout to show the collapsed representation.
+ *
+ * @param isCollapsed is it collapsed
+ */
+ @RemotableViewMethod
+ public void setIsCollapsed(boolean isCollapsed) {
+ mIsCollapsed = isCollapsed;
+ mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
+ updateExpandButton();
+ updateContentEndPaddings();
+ }
+
+ @RemotableViewMethod
+ public void setData(Bundle extras) {
+ Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
+ List<Notification.MessagingStyle.Message> newMessages
+ = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
+ Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
+ List<Notification.MessagingStyle.Message> newHistoricMessages
+ = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+
+ // mUser now set (would be nice to avoid the side effect but WHATEVER)
+ setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
+
+ // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
+ RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
+ extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ addRemoteInputHistoryToMessages(newMessages, history);
+
+ boolean showSpinner =
+ extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
+ // bind it, baby
+ bind(newMessages, newHistoricMessages, showSpinner);
+
+ int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
+ setUnreadCount(unreadCount);
+ }
+
+ @Override
+ public void setImageResolver(ImageResolver resolver) {
+ mImageResolver = resolver;
+ }
+
+ /** @hide */
+ public void setUnreadCount(int unreadCount) {
+ boolean visible = mIsCollapsed && unreadCount > 1;
+ mUnreadBadge.setVisibility(visible ? VISIBLE : GONE);
+ if (visible) {
+ CharSequence text = unreadCount >= 100
+ ? getResources().getString(R.string.unread_convo_overflow, 99)
+ : String.format(Locale.getDefault(), "%d", unreadCount);
+ mUnreadBadge.setText(text);
+ mUnreadBadge.setBackgroundTintList(ColorStateList.valueOf(mLayoutColor));
+ boolean needDarkText = ColorUtils.calculateLuminance(mLayoutColor) > 0.5f;
+ mUnreadBadge.setTextColor(needDarkText ? Color.BLACK : Color.WHITE);
+ }
+ }
+
+ private void addRemoteInputHistoryToMessages(
+ List<Notification.MessagingStyle.Message> newMessages,
+ RemoteInputHistoryItem[] remoteInputHistory) {
+ if (remoteInputHistory == null || remoteInputHistory.length == 0) {
+ return;
+ }
+ for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
+ RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
+ Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
+ historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
+ if (historyMessage.getUri() != null) {
+ message.setData(historyMessage.getMimeType(), historyMessage.getUri());
+ }
+ newMessages.add(message);
+ }
+ }
+
+ private void bind(List<Notification.MessagingStyle.Message> newMessages,
+ List<Notification.MessagingStyle.Message> newHistoricMessages,
+ boolean showSpinner) {
+ // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding
+ // if they exist
+ List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
+ true /* isHistoric */);
+ List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
+
+ // Copy our groups, before they get clobbered
+ ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
+
+ // Add our new MessagingMessages to groups
+ List<List<MessagingMessage>> groups = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
+
+ // Lets first find the groups (populate `groups` and `senders`)
+ findGroups(historicMessages, messages, groups, senders);
+
+ // Let's now create the views and reorder them accordingly
+ // side-effect: updates mGroups, mAddedGroups
+ createGroupViews(groups, senders, showSpinner);
+
+ // Let's first check which groups were removed altogether and remove them in one animation
+ removeGroups(oldGroups);
+
+ // Let's remove the remaining messages
+ mMessages.forEach(REMOVE_MESSAGE);
+ mHistoricMessages.forEach(REMOVE_MESSAGE);
+
+ mMessages = messages;
+ mHistoricMessages = historicMessages;
+
+ updateHistoricMessageVisibility();
+ updateTitleAndNamesDisplay();
+
+ updateConversationLayout();
+ }
+
+ /**
+ * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc);
+ */
+ private void updateConversationLayout() {
+ // Set avatar and name
+ CharSequence conversationText = mConversationTitle;
+ if (mIsOneToOne) {
+ // Let's resolve the icon / text from the last sender
+ mConversationIconView.setVisibility(VISIBLE);
+ mConversationFacePile.setVisibility(GONE);
+ CharSequence userKey = getKey(mUser);
+ for (int i = mGroups.size() - 1; i >= 0; i--) {
+ MessagingGroup messagingGroup = mGroups.get(i);
+ Person messageSender = messagingGroup.getSender();
+ if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender)))
+ || i == 0) {
+ if (TextUtils.isEmpty(conversationText)) {
+ // We use the sendername as header text if no conversation title is provided
+ // (This usually happens for most 1:1 conversations)
+ conversationText = messagingGroup.getSenderName();
+ }
+ Icon avatarIcon = messagingGroup.getAvatarIcon();
+ if (avatarIcon == null) {
+ avatarIcon = createAvatarSymbol(conversationText, "", mLayoutColor);
+ }
+ mConversationIcon = avatarIcon;
+ mConversationIconView.setImageIcon(mConversationIcon);
+ break;
+ }
+ }
+ } else {
+ if (mLargeIcon != null) {
+ mConversationIcon = mLargeIcon;
+ mConversationIconView.setVisibility(VISIBLE);
+ mConversationFacePile.setVisibility(GONE);
+ mConversationIconView.setImageIcon(mLargeIcon);
+ } else {
+ mConversationIcon = null;
+ mConversationIconView.setVisibility(GONE);
+ // This will also inflate it!
+ mConversationFacePile.setVisibility(VISIBLE);
+ // rebind the value to the inflated view instead of the stub
+ mConversationFacePile = findViewById(R.id.conversation_face_pile);
+ bindFacePile();
+ }
+ }
+ if (TextUtils.isEmpty(conversationText)) {
+ conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName;
+ }
+ mConversationText.setText(conversationText);
+ // Update if the groups can hide the sender if they are first (applies to 1:1 conversations)
+ // This needs to happen after all of the above o update all of the groups
+ for (int i = mGroups.size() - 1; i >= 0; i--) {
+ MessagingGroup messagingGroup = mGroups.get(i);
+ CharSequence messageSender = messagingGroup.getSenderName();
+ boolean canHide = mIsOneToOne
+ && TextUtils.equals(conversationText, messageSender);
+ messagingGroup.setCanHideSenderIfFirst(canHide);
+ }
+ updateAppName();
+ updateIconPositionAndSize();
+ updateImageMessages();
+ updatePaddingsBasedOnContentAvailability();
+ updateActionListPadding();
+ updateAppNameDividerVisibility();
+ }
+
+ private void updateActionListPadding() {
+ if (mActions == null) {
+ return;
+ }
+ View firstAction = mActions.getChildAt(0);
+ if (firstAction != null) {
+ // Let's visually position the first action where the content starts
+ int paddingStart = mConversationContentStart;
+
+ MarginLayoutParams layoutParams = (MarginLayoutParams) firstAction.getLayoutParams();
+ paddingStart -= layoutParams.getMarginStart();
+ paddingStart -= mInternalButtonPadding;
+
+ mActions.setPaddingRelative(paddingStart,
+ mActions.getPaddingTop(),
+ mActions.getPaddingEnd(),
+ mActions.getPaddingBottom());
+ }
+ }
+
+ private void updateImageMessages() {
+ View newMessage = null;
+ if (mIsCollapsed && mGroups.size() > 0) {
+
+ // When collapsed, we're displaying the image message in a dedicated container
+ // on the right of the layout instead of inline. Let's add the isolated image there
+ MessagingGroup messagingGroup = mGroups.get(mGroups.size() -1);
+ MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage();
+ if (isolatedMessage != null) {
+ newMessage = isolatedMessage.getView();
+ }
+ }
+ // Remove all messages that don't belong into the image layout
+ View previousMessage = mImageMessageContainer.getChildAt(0);
+ if (previousMessage != newMessage) {
+ mImageMessageContainer.removeView(previousMessage);
+ if (newMessage != null) {
+ mImageMessageContainer.addView(newMessage);
+ }
+ }
+ mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE);
+ }
+
+ public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) {
+ applyNotificationBackgroundColor(bottomBackground);
+ // Let's find the two last conversations:
+ Icon secondLastIcon = null;
+ CharSequence lastKey = null;
+ Icon lastIcon = null;
+ CharSequence userKey = getKey(mUser);
+ for (int i = mGroups.size() - 1; i >= 0; i--) {
+ MessagingGroup messagingGroup = mGroups.get(i);
+ Person messageSender = messagingGroup.getSender();
+ boolean notUser = messageSender != null
+ && !TextUtils.equals(userKey, getKey(messageSender));
+ boolean notIncluded = messageSender != null
+ && !TextUtils.equals(lastKey, getKey(messageSender));
+ if ((notUser && notIncluded)
+ || (i == 0 && lastKey == null)) {
+ if (lastIcon == null) {
+ lastIcon = messagingGroup.getAvatarIcon();
+ lastKey = getKey(messageSender);
+ } else {
+ secondLastIcon = messagingGroup.getAvatarIcon();
+ break;
+ }
+ }
+ }
+ if (lastIcon == null) {
+ lastIcon = createAvatarSymbol(" ", "", mLayoutColor);
+ }
+ bottomView.setImageIcon(lastIcon);
+ if (secondLastIcon == null) {
+ secondLastIcon = createAvatarSymbol("", "", mLayoutColor);
+ }
+ topView.setImageIcon(secondLastIcon);
+ }
+
+ private void bindFacePile() {
+ ImageView bottomBackground = mConversationFacePile.findViewById(
+ R.id.conversation_face_pile_bottom_background);
+ ImageView bottomView = mConversationFacePile.findViewById(
+ R.id.conversation_face_pile_bottom);
+ ImageView topView = mConversationFacePile.findViewById(
+ R.id.conversation_face_pile_top);
+
+ bindFacePile(bottomBackground, bottomView, topView);
+
+ int conversationAvatarSize;
+ int facepileAvatarSize;
+ int facePileBackgroundSize;
+ if (mIsCollapsed) {
+ conversationAvatarSize = mConversationAvatarSize;
+ facepileAvatarSize = mFacePileAvatarSize;
+ facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth;
+ } else {
+ conversationAvatarSize = mConversationAvatarSizeExpanded;
+ facepileAvatarSize = mFacePileAvatarSizeExpandedGroup;
+ facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded;
+ }
+ LayoutParams layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
+ layoutParams.width = conversationAvatarSize;
+ layoutParams.height = conversationAvatarSize;
+ mConversationFacePile.setLayoutParams(layoutParams);
+
+ layoutParams = (LayoutParams) bottomView.getLayoutParams();
+ layoutParams.width = facepileAvatarSize;
+ layoutParams.height = facepileAvatarSize;
+ bottomView.setLayoutParams(layoutParams);
+
+ layoutParams = (LayoutParams) topView.getLayoutParams();
+ layoutParams.width = facepileAvatarSize;
+ layoutParams.height = facepileAvatarSize;
+ topView.setLayoutParams(layoutParams);
+
+ layoutParams = (LayoutParams) bottomBackground.getLayoutParams();
+ layoutParams.width = facePileBackgroundSize;
+ layoutParams.height = facePileBackgroundSize;
+ bottomBackground.setLayoutParams(layoutParams);
+ }
+
+ private void updateAppName() {
+ mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE);
+ }
+
+ public boolean shouldHideAppName() {
+ return mIsCollapsed;
+ }
+
+ /**
+ * update the icon position and sizing
+ */
+ private void updateIconPositionAndSize() {
+ int sidemargin;
+ int conversationAvatarSize;
+ if (mIsOneToOne || mIsCollapsed) {
+ sidemargin = mBadgedSideMargins;
+ conversationAvatarSize = mConversationAvatarSize;
+ } else {
+ sidemargin = mConversationFacePile.getVisibility() == VISIBLE
+ ? mExpandedGroupSideMarginFacePile
+ : mExpandedGroupSideMargin;
+ conversationAvatarSize = mConversationAvatarSizeExpanded;
+ }
+ LayoutParams layoutParams =
+ (LayoutParams) mConversationIconBadge.getLayoutParams();
+ layoutParams.topMargin = sidemargin;
+ layoutParams.setMarginStart(sidemargin);
+ mConversationIconBadge.setLayoutParams(layoutParams);
+
+ if (mConversationIconView.getVisibility() == VISIBLE) {
+ layoutParams = (LayoutParams) mConversationIconView.getLayoutParams();
+ layoutParams.width = conversationAvatarSize;
+ layoutParams.height = conversationAvatarSize;
+ mConversationIconView.setLayoutParams(layoutParams);
+ }
+ }
+
+ private void updatePaddingsBasedOnContentAvailability() {
+ int messagingPadding = mIsOneToOne || mIsCollapsed
+ ? 0
+ // Add some extra padding to the messages, since otherwise it will overlap with the
+ // group
+ : mExpandedGroupMessagePadding;
+
+ mConversationIconContainer.setPaddingRelative(
+ mConversationIconContainer.getPaddingStart(),
+ mConversationIconTopPadding,
+ mConversationIconContainer.getPaddingEnd(),
+ mConversationIconContainer.getPaddingBottom());
+
+ mMessagingLinearLayout.setPaddingRelative(
+ mMessagingLinearLayout.getPaddingStart(),
+ messagingPadding,
+ mMessagingLinearLayout.getPaddingEnd(),
+ mMessagingLinearLayout.getPaddingBottom());
+ }
+
+ @RemotableViewMethod
+ public void setLargeIcon(Icon largeIcon) {
+ mLargeIcon = largeIcon;
+ }
+
+ /**
+ * Sets the conversation title of this conversation.
+ *
+ * @param conversationTitle the conversation title
+ */
+ @RemotableViewMethod
+ public void setConversationTitle(CharSequence conversationTitle) {
+ mConversationTitle = conversationTitle;
+ }
+
+ public CharSequence getConversationTitle() {
+ return mConversationText.getText();
+ }
+
+ private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
+ int size = oldGroups.size();
+ for (int i = 0; i < size; i++) {
+ MessagingGroup group = oldGroups.get(i);
+ if (!mGroups.contains(group)) {
+ List<MessagingMessage> messages = group.getMessages();
+ Runnable endRunnable = () -> {
+ mMessagingLinearLayout.removeTransientView(group);
+ group.recycle();
+ };
+
+ boolean wasShown = group.isShown();
+ mMessagingLinearLayout.removeView(group);
+ if (wasShown && !MessagingLinearLayout.isGone(group)) {
+ mMessagingLinearLayout.addTransientView(group, 0);
+ group.removeGroupAnimated(endRunnable);
+ } else {
+ endRunnable.run();
+ }
+ mMessages.removeAll(messages);
+ mHistoricMessages.removeAll(messages);
+ }
+ }
+ }
+
+ private void updateTitleAndNamesDisplay() {
+ ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
+ ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
+ for (int i = 0; i < mGroups.size(); i++) {
+ MessagingGroup group = mGroups.get(i);
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
+ continue;
+ }
+ if (!uniqueNames.containsKey(senderName)) {
+ // Only use visible characters to get uniqueNames
+ String pureSenderName = IGNORABLE_CHAR_PATTERN
+ .matcher(senderName).replaceAll("" /* replacement */);
+ char c = pureSenderName.charAt(0);
+ if (uniqueCharacters.containsKey(c)) {
+ // this character was already used, lets make it more unique. We first need to
+ // resolve the existing character if it exists
+ CharSequence existingName = uniqueCharacters.get(c);
+ if (existingName != null) {
+ uniqueNames.put(existingName, findNameSplit((String) existingName));
+ uniqueCharacters.put(c, null);
+ }
+ uniqueNames.put(senderName, findNameSplit((String) senderName));
+ } else {
+ uniqueNames.put(senderName, Character.toString(c));
+ uniqueCharacters.put(c, pureSenderName);
+ }
+ }
+ }
+
+ // Now that we have the correct symbols, let's look what we have cached
+ ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
+ for (int i = 0; i < mGroups.size(); i++) {
+ // Let's now set the avatars
+ MessagingGroup group = mGroups.get(i);
+ boolean isOwnMessage = group.getSender() == mUser;
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
+ || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
+ continue;
+ }
+ String symbol = uniqueNames.get(senderName);
+ Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
+ symbol, mLayoutColor);
+ if (cachedIcon != null) {
+ cachedAvatars.put(senderName, cachedIcon);
+ }
+ }
+
+ for (int i = 0; i < mGroups.size(); i++) {
+ // Let's now set the avatars
+ MessagingGroup group = mGroups.get(i);
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
+ continue;
+ }
+ if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
+ group.setAvatar(mAvatarReplacement);
+ } else {
+ Icon cachedIcon = cachedAvatars.get(senderName);
+ if (cachedIcon == null) {
+ cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
+ mLayoutColor);
+ cachedAvatars.put(senderName, cachedIcon);
+ }
+ group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
+ mLayoutColor);
+ }
+ }
+ }
+
+ private Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
+ if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
+ SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
+ Icon avatarIcon = Icon.createWithResource(getContext(),
+ R.drawable.messaging_user);
+ avatarIcon.setTint(findColor(senderName, layoutColor));
+ return avatarIcon;
+ } else {
+ Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ float radius = mAvatarSize / 2.0f;
+ int color = findColor(senderName, layoutColor);
+ mPaint.setColor(color);
+ canvas.drawCircle(radius, radius, radius, mPaint);
+ boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
+ mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
+ mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
+ int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
+ canvas.drawText(symbol, radius, yPos, mTextPaint);
+ return Icon.createWithBitmap(bitmap);
+ }
+ }
+
+ private int findColor(CharSequence senderName, int layoutColor) {
+ double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
+ float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
+
+ // we need to offset the range if the luminance is too close to the borders
+ shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
+ shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
+ return ContrastColorUtil.getShiftedColor(layoutColor,
+ (int) (shift * COLOR_SHIFT_AMOUNT));
+ }
+
+ private String findNameSplit(String existingName) {
+ String[] split = existingName.split(" ");
+ if (split.length > 1) {
+ return Character.toString(split[0].charAt(0))
+ + Character.toString(split[1].charAt(0));
+ }
+ return existingName.substring(0, 1);
+ }
+
+ @RemotableViewMethod
+ public void setLayoutColor(int color) {
+ mLayoutColor = color;
+ }
+
+ @RemotableViewMethod
+ public void setIsOneToOne(boolean oneToOne) {
+ mIsOneToOne = oneToOne;
+ }
+
+ @RemotableViewMethod
+ public void setSenderTextColor(int color) {
+ mSenderTextColor = color;
+ mConversationText.setTextColor(color);
+ }
+
+ /**
+ * @param color the color of the notification background
+ */
+ @RemotableViewMethod
+ public void setNotificationBackgroundColor(int color) {
+ mNotificationBackgroundColor = color;
+ applyNotificationBackgroundColor(mConversationIconBadgeBg);
+ }
+
+ private void applyNotificationBackgroundColor(ImageView view) {
+ view.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor));
+ }
+
+ @RemotableViewMethod
+ public void setMessageTextColor(int color) {
+ mMessageTextColor = color;
+ }
+
+ private void setUser(Person user) {
+ mUser = user;
+ if (mUser.getIcon() == null) {
+ Icon userIcon = Icon.createWithResource(getContext(),
+ R.drawable.messaging_user);
+ userIcon.setTint(mLayoutColor);
+ mUser = mUser.toBuilder().setIcon(userIcon).build();
+ }
+ }
+
+ private void createGroupViews(List<List<MessagingMessage>> groups,
+ List<Person> senders, boolean showSpinner) {
+ mGroups.clear();
+ for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
+ List<MessagingMessage> group = groups.get(groupIndex);
+ MessagingGroup newGroup = null;
+ // we'll just take the first group that exists or create one there is none
+ for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
+ MessagingMessage message = group.get(messageIndex);
+ newGroup = message.getGroup();
+ if (newGroup != null) {
+ break;
+ }
+ }
+ // Create a new group, adding it to the linear layout as well
+ if (newGroup == null) {
+ newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
+ mAddedGroups.add(newGroup);
+ }
+ newGroup.setImageDisplayLocation(mIsCollapsed
+ ? IMAGE_DISPLAY_LOCATION_EXTERNAL
+ : IMAGE_DISPLAY_LOCATION_INLINE);
+ newGroup.setIsInConversation(true);
+ newGroup.setLayoutColor(mLayoutColor);
+ newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
+ Person sender = senders.get(groupIndex);
+ CharSequence nameOverride = null;
+ if (sender != mUser && mNameReplacement != null) {
+ nameOverride = mNameReplacement;
+ }
+ newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed);
+ newGroup.setSingleLine(mIsCollapsed);
+ newGroup.setSender(sender, nameOverride);
+ newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
+ mGroups.add(newGroup);
+
+ // Reposition to the correct place (if we're re-using a group)
+ if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
+ mMessagingLinearLayout.removeView(newGroup);
+ mMessagingLinearLayout.addView(newGroup, groupIndex);
+ }
+ newGroup.setMessages(group);
+ }
+ }
+
+ private void findGroups(List<MessagingMessage> historicMessages,
+ List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
+ List<Person> senders) {
+ CharSequence currentSenderKey = null;
+ List<MessagingMessage> currentGroup = null;
+ int histSize = historicMessages.size();
+ for (int i = 0; i < histSize + messages.size(); i++) {
+ MessagingMessage message;
+ if (i < histSize) {
+ message = historicMessages.get(i);
+ } else {
+ message = messages.get(i - histSize);
+ }
+ boolean isNewGroup = currentGroup == null;
+ Person sender = message.getMessage().getSenderPerson();
+ CharSequence key = getKey(sender);
+ isNewGroup |= !TextUtils.equals(key, currentSenderKey);
+ if (isNewGroup) {
+ currentGroup = new ArrayList<>();
+ groups.add(currentGroup);
+ if (sender == null) {
+ sender = mUser;
+ }
+ senders.add(sender);
+ currentSenderKey = key;
+ }
+ currentGroup.add(message);
+ }
+ }
+
+ private CharSequence getKey(Person person) {
+ return person == null ? null : person.getKey() == null ? person.getName() : person.getKey();
+ }
+
+ /**
+ * Creates new messages, reusing existing ones if they are available.
+ *
+ * @param newMessages the messages to parse.
+ */
+ private List<MessagingMessage> createMessages(
+ List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
+ List<MessagingMessage> result = new ArrayList<>();
+ for (int i = 0; i < newMessages.size(); i++) {
+ Notification.MessagingStyle.Message m = newMessages.get(i);
+ MessagingMessage message = findAndRemoveMatchingMessage(m);
+ if (message == null) {
+ message = MessagingMessage.createMessage(this, m, mImageResolver);
+ }
+ message.setIsHistoric(historic);
+ result.add(message);
+ }
+ return result;
+ }
+
+ private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
+ for (int i = 0; i < mMessages.size(); i++) {
+ MessagingMessage existing = mMessages.get(i);
+ if (existing.sameAs(m)) {
+ mMessages.remove(i);
+ return existing;
+ }
+ }
+ for (int i = 0; i < mHistoricMessages.size(); i++) {
+ MessagingMessage existing = mHistoricMessages.get(i);
+ if (existing.sameAs(m)) {
+ mHistoricMessages.remove(i);
+ return existing;
+ }
+ }
+ return null;
+ }
+
+ public void showHistoricMessages(boolean show) {
+ mShowHistoricMessages = show;
+ updateHistoricMessageVisibility();
+ }
+
+ private void updateHistoricMessageVisibility() {
+ int numHistoric = mHistoricMessages.size();
+ for (int i = 0; i < numHistoric; i++) {
+ MessagingMessage existing = mHistoricMessages.get(i);
+ existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
+ }
+ int numGroups = mGroups.size();
+ for (int i = 0; i < numGroups; i++) {
+ MessagingGroup group = mGroups.get(i);
+ int visibleChildren = 0;
+ List<MessagingMessage> messages = group.getMessages();
+ int numGroupMessages = messages.size();
+ for (int j = 0; j < numGroupMessages; j++) {
+ MessagingMessage message = messages.get(j);
+ if (message.getVisibility() != GONE) {
+ visibleChildren++;
+ }
+ }
+ if (visibleChildren > 0 && group.getVisibility() == GONE) {
+ group.setVisibility(VISIBLE);
+ } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
+ group.setVisibility(GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (!mAddedGroups.isEmpty()) {
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ for (MessagingGroup group : mAddedGroups) {
+ if (!group.isShown()) {
+ continue;
+ }
+ MessagingPropertyAnimator.fadeIn(group.getAvatar());
+ MessagingPropertyAnimator.fadeIn(group.getSenderView());
+ MessagingPropertyAnimator.startLocalTranslationFrom(group,
+ group.getHeight(), LINEAR_OUT_SLOW_IN);
+ }
+ mAddedGroups.clear();
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ });
+ }
+ if (mAppOps.getWidth() > 0) {
+
+ // Let's increase the touch size of the app ops view if it's here
+ mAppOpsTouchRect.set(
+ mAppOps.getLeft(),
+ mAppOps.getTop(),
+ mAppOps.getRight(),
+ mAppOps.getBottom());
+ for (int i = 0; i < mAppOps.getChildCount(); i++) {
+ View child = mAppOps.getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ // Make sure each child has at least a minTouchSize touch target around it
+ float childTouchLeft = child.getLeft() + child.getWidth() / 2.0f
+ - mMinTouchSize / 2.0f;
+ float childTouchRight = childTouchLeft + mMinTouchSize;
+ mAppOpsTouchRect.left = (int) Math.min(mAppOpsTouchRect.left,
+ mAppOps.getLeft() + childTouchLeft);
+ mAppOpsTouchRect.right = (int) Math.max(mAppOpsTouchRect.right,
+ mAppOps.getLeft() + childTouchRight);
+ }
+
+ // Increase the height
+ int heightIncrease = 0;
+ if (mAppOpsTouchRect.height() < mMinTouchSize) {
+ heightIncrease = (int) Math.ceil((mMinTouchSize - mAppOpsTouchRect.height())
+ / 2.0f);
+ }
+ mAppOpsTouchRect.inset(0, -heightIncrease);
+
+ // Let's adjust the hitrect since app ops isn't a direct child
+ ViewGroup viewGroup = (ViewGroup) mAppOps.getParent();
+ while (viewGroup != this) {
+ mAppOpsTouchRect.offset(viewGroup.getLeft(), viewGroup.getTop());
+ viewGroup = (ViewGroup) viewGroup.getParent();
+ }
+ //
+ // Extend the size of the app opps to be at least 48dp
+ setTouchDelegate(new TouchDelegate(mAppOpsTouchRect, mAppOps));
+ }
+ }
+
+ public MessagingLinearLayout getMessagingLinearLayout() {
+ return mMessagingLinearLayout;
+ }
+
+ public @NonNull ViewGroup getImageMessageContainer() {
+ return mImageMessageContainer;
+ }
+
+ public ArrayList<MessagingGroup> getMessagingGroups() {
+ return mGroups;
+ }
+
+ private void updateExpandButton() {
+ int drawableId;
+ int contentDescriptionId;
+ int gravity;
+ int topMargin = 0;
+ ViewGroup newContainer;
+ if (mIsCollapsed) {
+ drawableId = R.drawable.ic_expand_notification;
+ contentDescriptionId = R.string.expand_button_content_description_collapsed;
+ gravity = Gravity.CENTER;
+ newContainer = mExpandButtonAndContentContainer;
+ } else {
+ drawableId = R.drawable.ic_collapse_notification;
+ contentDescriptionId = R.string.expand_button_content_description_expanded;
+ gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+ topMargin = mExpandButtonExpandedTopMargin;
+ newContainer = this;
+ }
+ mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
+ mExpandButton.setColorFilter(mExpandButton.getOriginalNotificationColor());
+
+ // We need to make sure that the expand button is in the linearlayout pushing over the
+ // content when collapsed, but allows the content to flow under it when expanded.
+ if (newContainer != mExpandButtonContainer.getParent()) {
+ ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer);
+ newContainer.addView(mExpandButtonContainer);
+ }
+
+ // update if the expand button is centered
+ LinearLayout.LayoutParams layoutParams =
+ (LinearLayout.LayoutParams) mExpandButton.getLayoutParams();
+ layoutParams.gravity = gravity;
+ layoutParams.topMargin = topMargin;
+ mExpandButton.setLayoutParams(layoutParams);
+
+ mExpandButtonInnerContainer.setContentDescription(mContext.getText(contentDescriptionId));
+ }
+
+ private void updateContentEndPaddings() {
+
+ // Let's make sure the conversation header can't run into the expand button when we're
+ // collapsed and update the paddings of the content
+ int headerPaddingEnd;
+ int contentPaddingEnd;
+ if (!mExpandable) {
+ headerPaddingEnd = 0;
+ contentPaddingEnd = mContentMarginEnd;
+ } else if (mIsCollapsed) {
+ headerPaddingEnd = 0;
+ contentPaddingEnd = 0;
+ } else {
+ headerPaddingEnd = mNotificationHeaderExpandedPadding;
+ contentPaddingEnd = mContentMarginEnd;
+ }
+ mConversationHeader.setPaddingRelative(
+ mConversationHeader.getPaddingStart(),
+ mConversationHeader.getPaddingTop(),
+ headerPaddingEnd,
+ mConversationHeader.getPaddingBottom());
+
+ mContentContainer.setPaddingRelative(
+ mContentContainer.getPaddingStart(),
+ mContentContainer.getPaddingTop(),
+ contentPaddingEnd,
+ mContentContainer.getPaddingBottom());
+ }
+
+ private void onAppNameVisibilityChanged() {
+ boolean appNameGone = mAppName.getVisibility() == GONE;
+ if (appNameGone != mAppNameGone) {
+ mAppNameGone = appNameGone;
+ updateAppNameDividerVisibility();
+ }
+ }
+
+ private void updateAppNameDividerVisibility() {
+ mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE);
+ }
+
+ public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) {
+ mExpandable = expandable;
+ if (expandable) {
+ mExpandButtonContainer.setVisibility(VISIBLE);
+ mExpandButtonInnerContainer.setOnClickListener(onClickListener);
+ } else {
+ // TODO: handle content paddings to end of layout
+ mExpandButtonContainer.setVisibility(GONE);
+ }
+ updateContentEndPaddings();
+ }
+
+ @Override
+ public void setMessagingClippingDisabled(boolean clippingDisabled) {
+ mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect);
+ }
+
+ @Nullable
+ public CharSequence getConversationSenderName() {
+ if (mGroups.isEmpty()) {
+ return null;
+ }
+ final CharSequence name = mGroups.get(mGroups.size() - 1).getSenderName();
+ return getResources().getString(R.string.conversation_single_line_name_display, name);
+ }
+
+ public boolean isOneToOne() {
+ return mIsOneToOne;
+ }
+
+ @Nullable
+ public CharSequence getConversationText() {
+ if (mMessages.isEmpty()) {
+ return null;
+ }
+ final MessagingMessage messagingMessage = mMessages.get(mMessages.size() - 1);
+ final CharSequence text = messagingMessage.getMessage().getText();
+ if (text == null && messagingMessage instanceof MessagingImageMessage) {
+ final String unformatted =
+ getResources().getString(R.string.conversation_single_line_image_placeholder);
+ SpannableString spannableString = new SpannableString(unformatted);
+ spannableString.setSpan(
+ new StyleSpan(Typeface.ITALIC),
+ 0,
+ spannableString.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ return spannableString;
+ }
+ return text;
+ }
+
+ @Nullable
+ public Icon getConversationIcon() {
+ return mConversationIcon;
+ }
+}
diff --git a/com/android/internal/widget/DecorCaptionView.java b/com/android/internal/widget/DecorCaptionView.java
new file mode 100644
index 0000000..2102145
--- /dev/null
+++ b/com/android/internal/widget/DecorCaptionView.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.Window;
+
+import com.android.internal.R;
+import com.android.internal.policy.DecorView;
+import com.android.internal.policy.PhoneWindow;
+
+import java.util.ArrayList;
+
+/**
+ * This class represents the special screen elements to control a window on freeform
+ * environment.
+ * As such this class handles the following things:
+ * <ul>
+ * <li>The caption, containing the system buttons like maximize, close and such as well as
+ * allowing the user to drag the window around.</li>
+ * </ul>
+ * After creating the view, the function {@link #setPhoneWindow} needs to be called to make
+ * the connection to it's owning PhoneWindow.
+ * Note: At this time the application can change various attributes of the DecorView which
+ * will break things (in subtle/unexpected ways):
+ * <ul>
+ * <li>setOutlineProvider</li>
+ * <li>setSurfaceFormat</li>
+ * <li>..</li>
+ * </ul>
+ *
+ * Here describe the behavior of overlaying caption on the content and drawing.
+ *
+ * First, no matter where the content View gets added, it will always be the first child and the
+ * caption will be the second. This way the caption will always be drawn on top of the content when
+ * overlaying is enabled.
+ *
+ * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch
+ * is dispatched on the caption area while overlaying it on content:
+ * <ul>
+ * <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the
+ * down action is performed on top close or maximize buttons; the reason for that is we want these
+ * buttons to always work.</li>
+ * <li>The caption view will try to consume the event to apply the dragging logic.</li>
+ * <li>If the touch event is not consumed by the caption, the content View will receive the touch
+ * event</li>
+ * </ul>
+ */
+public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
+ GestureDetector.OnGestureListener {
+ private final static String TAG = "DecorCaptionView";
+ private PhoneWindow mOwner = null;
+ private boolean mShow = false;
+
+ // True if the window is being dragged.
+ private boolean mDragging = false;
+
+ private boolean mOverlayWithAppContent = false;
+
+ private View mCaption;
+ private View mContent;
+ private View mMaximize;
+ private View mClose;
+
+ // Fields for detecting drag events.
+ private int mTouchDownX;
+ private int mTouchDownY;
+ private boolean mCheckForDragging;
+ private int mDragSlop;
+
+ // Fields for detecting and intercepting click events on close/maximize.
+ private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
+ // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
+ // with existing click detection.
+ private GestureDetector mGestureDetector;
+ private final Rect mCloseRect = new Rect();
+ private final Rect mMaximizeRect = new Rect();
+ private View mClickTarget;
+ private int mRootScrollY;
+
+ public DecorCaptionView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public DecorCaptionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ private void init(Context context) {
+ mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mGestureDetector = new GestureDetector(context, this);
+ setContentDescription(context.getString(R.string.accessibility_freeform_caption,
+ context.getPackageManager().getApplicationLabel(context.getApplicationInfo())));
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mCaption = getChildAt(0);
+ }
+
+ public void setPhoneWindow(PhoneWindow owner, boolean show) {
+ mOwner = owner;
+ mShow = show;
+ mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
+ updateCaptionVisibility();
+ // By changing the outline provider to BOUNDS, the window can remove its
+ // background without removing the shadow.
+ mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
+ mMaximize = findViewById(R.id.maximize_window);
+ mClose = findViewById(R.id.close_window);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ // If the user starts touch on the maximize/close buttons, we immediately intercept, so
+ // that these buttons are always clickable.
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+ // Only offset y for containment tests because the actual views are already translated.
+ if (mMaximizeRect.contains(x, y - mRootScrollY)) {
+ mClickTarget = mMaximize;
+ }
+ if (mCloseRect.contains(x, y - mRootScrollY)) {
+ mClickTarget = mClose;
+ }
+ }
+ return mClickTarget != null;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mClickTarget != null) {
+ mGestureDetector.onTouchEvent(event);
+ final int action = event.getAction();
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mClickTarget = null;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent e) {
+ // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
+ // the old input device events get cancelled first. So no need to remember the kind of
+ // input device we are listening to.
+ final int x = (int) e.getX();
+ final int y = (int) e.getY();
+ final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE;
+ final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0;
+ final int actionMasked = e.getActionMasked();
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ if (!mShow) {
+ // When there is no caption we should not react to anything.
+ return false;
+ }
+ // Checking for a drag action is started if we aren't dragging already and the
+ // starting event is either a left mouse button or any other input device.
+ if (!fromMouse || primaryButton) {
+ mCheckForDragging = true;
+ mTouchDownX = x;
+ mTouchDownY = y;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) {
+ mCheckForDragging = false;
+ mDragging = true;
+ startMovingTask(e.getRawX(), e.getRawY());
+ // After the above call the framework will take over the input.
+ // This handler will receive ACTION_CANCEL soon (possible after a few spurious
+ // ACTION_MOVE events which are safe to ignore).
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (!mDragging) {
+ break;
+ }
+ // Abort the ongoing dragging.
+ if (actionMasked == MotionEvent.ACTION_UP) {
+ // If it receives ACTION_UP event, the dragging is already finished and also
+ // the system can not end drag on ACTION_UP event. So request to finish
+ // dragging.
+ finishMovingTask();
+ }
+ mDragging = false;
+ return !mCheckForDragging;
+ }
+ return mDragging || mCheckForDragging;
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ private boolean passedSlop(int x, int y) {
+ return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
+ }
+
+ /**
+ * The phone window configuration has changed and the caption needs to be updated.
+ * @param show True if the caption should be shown.
+ */
+ public void onConfigurationChanged(boolean show) {
+ mShow = show;
+ updateCaptionVisibility();
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (!(params instanceof MarginLayoutParams)) {
+ throw new IllegalArgumentException(
+ "params " + params + " must subclass MarginLayoutParams");
+ }
+ // Make sure that we never get more then one client area in our view.
+ if (index >= 2 || getChildCount() >= 2) {
+ throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
+ }
+ // To support the overlaying content in the caption, we need to put the content view as the
+ // first child to get the right Z-Ordering.
+ super.addView(child, 0, params);
+ mContent = child;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int captionHeight;
+ if (mCaption.getVisibility() != View.GONE) {
+ measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ captionHeight = mCaption.getMeasuredHeight();
+ } else {
+ captionHeight = 0;
+ }
+ if (mContent != null) {
+ if (mOverlayWithAppContent) {
+ measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ } else {
+ measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
+ captionHeight);
+ }
+ }
+
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int captionHeight;
+ if (mCaption.getVisibility() != View.GONE) {
+ mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
+ captionHeight = mCaption.getBottom() - mCaption.getTop();
+ mMaximize.getHitRect(mMaximizeRect);
+ mClose.getHitRect(mCloseRect);
+ } else {
+ captionHeight = 0;
+ mMaximizeRect.setEmpty();
+ mCloseRect.setEmpty();
+ }
+
+ if (mContent != null) {
+ if (mOverlayWithAppContent) {
+ mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
+ } else {
+ mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
+ captionHeight + mContent.getMeasuredHeight());
+ }
+ }
+
+ ((DecorView) mOwner.getDecorView()).notifyCaptionHeightChanged();
+
+ // This assumes that the caption bar is at the top.
+ mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
+ mClose.getRight(), mClose.getBottom());
+ }
+
+ /**
+ * Updates the visibility of the caption.
+ **/
+ private void updateCaptionVisibility() {
+ mCaption.setVisibility(mShow ? VISIBLE : GONE);
+ mCaption.setOnTouchListener(this);
+ }
+
+ /**
+ * Maximize or restore the window by moving it to the maximized or freeform workspace stack.
+ **/
+ private void toggleFreeformWindowingMode() {
+ Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
+ if (callback != null) {
+ try {
+ callback.toggleFreeformWindowingMode();
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Cannot change task workspace.");
+ }
+ }
+ }
+
+ public boolean isCaptionShowing() {
+ return mShow;
+ }
+
+ public int getCaptionHeight() {
+ return (mCaption != null) ? mCaption.getHeight() : 0;
+ }
+
+ public void removeContentView() {
+ if (mContent != null) {
+ removeView(mContent);
+ mContent = null;
+ }
+ }
+
+ public View getCaption() {
+ return mCaption;
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
+ MarginLayoutParams.MATCH_PARENT);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(LayoutParams p) {
+ return new MarginLayoutParams(p);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof MarginLayoutParams;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (mClickTarget == mMaximize) {
+ toggleFreeformWindowingMode();
+ } else if (mClickTarget == mClose) {
+ mOwner.dispatchOnWindowDismissed(
+ true /*finishTask*/, false /*suppressWindowTransition*/);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ /**
+ * Called when {@link android.view.ViewRootImpl} scrolls for adjustPan.
+ */
+ public void onRootViewScrollYChanged(int scrollY) {
+ // Offset the caption opposite the root scroll. This keeps the caption at the
+ // top of the window during adjustPan.
+ if (mCaption != null) {
+ mRootScrollY = scrollY;
+ mCaption.setTranslationY(scrollY);
+ }
+ }
+}
diff --git a/com/android/internal/widget/DecorContentParent.java b/com/android/internal/widget/DecorContentParent.java
new file mode 100644
index 0000000..ac524f9
--- /dev/null
+++ b/com/android/internal/widget/DecorContentParent.java
@@ -0,0 +1,52 @@
+/*
+ * 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.internal.widget;
+
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.Menu;
+import android.view.Window;
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Implemented by the top-level decor layout for a window. DecorContentParent offers
+ * entry points for a number of title/window decor features.
+ */
+public interface DecorContentParent {
+ void setWindowCallback(Window.Callback cb);
+ void setWindowTitle(CharSequence title);
+ CharSequence getTitle();
+ void initFeature(int windowFeature);
+ void setUiOptions(int uiOptions);
+ boolean hasIcon();
+ boolean hasLogo();
+ void setIcon(int resId);
+ void setIcon(Drawable d);
+ void setLogo(int resId);
+ boolean canShowOverflowMenu();
+ boolean isOverflowMenuShowing();
+ boolean isOverflowMenuShowPending();
+ boolean showOverflowMenu();
+ boolean hideOverflowMenu();
+ void setMenuPrepared();
+ void setMenu(Menu menu, MenuPresenter.Callback cb);
+ void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
+ void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
+ void dismissPopups();
+}
diff --git a/com/android/internal/widget/DecorToolbar.java b/com/android/internal/widget/DecorToolbar.java
new file mode 100644
index 0000000..fe70d7b
--- /dev/null
+++ b/com/android/internal/widget/DecorToolbar.java
@@ -0,0 +1,107 @@
+/*
+ * 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.internal.widget;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.SpinnerAdapter;
+
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Common interface for a toolbar that sits as part of the window decor.
+ * Layouts that control window decor use this as a point of interaction with different
+ * bar implementations.
+ *
+ * @hide
+ */
+public interface DecorToolbar {
+ ViewGroup getViewGroup();
+ Context getContext();
+ boolean isSplit();
+ boolean hasExpandedActionView();
+ void collapseActionView();
+ void setWindowCallback(Window.Callback cb);
+ void setWindowTitle(CharSequence title);
+ CharSequence getTitle();
+ void setTitle(CharSequence title);
+ CharSequence getSubtitle();
+ void setSubtitle(CharSequence subtitle);
+ void initProgress();
+ void initIndeterminateProgress();
+ boolean canSplit();
+ void setSplitView(ViewGroup splitView);
+ void setSplitToolbar(boolean split);
+ void setSplitWhenNarrow(boolean splitWhenNarrow);
+ boolean hasIcon();
+ boolean hasLogo();
+ void setIcon(int resId);
+ void setIcon(Drawable d);
+ void setLogo(int resId);
+ void setLogo(Drawable d);
+ boolean canShowOverflowMenu();
+ boolean isOverflowMenuShowing();
+ boolean isOverflowMenuShowPending();
+ boolean showOverflowMenu();
+ boolean hideOverflowMenu();
+ void setMenuPrepared();
+ void setMenu(Menu menu, MenuPresenter.Callback cb);
+ void dismissPopupMenus();
+
+ int getDisplayOptions();
+ void setDisplayOptions(int opts);
+ void setEmbeddedTabView(ScrollingTabContainerView tabView);
+ boolean hasEmbeddedTabs();
+ boolean isTitleTruncated();
+ void setCollapsible(boolean collapsible);
+ void setHomeButtonEnabled(boolean enable);
+ int getNavigationMode();
+ void setNavigationMode(int mode);
+ void setDropdownParams(SpinnerAdapter adapter, AdapterView.OnItemSelectedListener listener);
+ void setDropdownSelectedPosition(int position);
+ int getDropdownSelectedPosition();
+ int getDropdownItemCount();
+ void setCustomView(View view);
+ View getCustomView();
+ void animateToVisibility(int visibility);
+ Animator setupAnimatorToVisibility(int visibility, long duration);
+ void setNavigationIcon(Drawable icon);
+ void setNavigationIcon(int resId);
+ void setNavigationContentDescription(CharSequence description);
+ void setNavigationContentDescription(int resId);
+ void setDefaultNavigationContentDescription(int defaultNavigationContentDescription);
+ void setDefaultNavigationIcon(Drawable icon);
+ void saveHierarchyState(SparseArray<Parcelable> toolbarStates);
+ void restoreHierarchyState(SparseArray<Parcelable> toolbarStates);
+ void setBackgroundDrawable(Drawable d);
+ int getHeight();
+ void setVisibility(int visible);
+ int getVisibility();
+ void setMenuCallbacks(MenuPresenter.Callback presenterCallback,
+ MenuBuilder.Callback menuBuilderCallback);
+ Menu getMenu();
+}
diff --git a/com/android/internal/widget/DefaultItemAnimator.java b/com/android/internal/widget/DefaultItemAnimator.java
new file mode 100644
index 0000000..92345af
--- /dev/null
+++ b/com/android/internal/widget/DefaultItemAnimator.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+
+import com.android.internal.widget.RecyclerView.ViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This implementation of {@link RecyclerView.ItemAnimator} provides basic
+ * animations on remove, add, and move events that happen to the items in
+ * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
+ *
+ * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
+ */
+public class DefaultItemAnimator extends SimpleItemAnimator {
+ private static final boolean DEBUG = false;
+
+ private static TimeInterpolator sDefaultInterpolator;
+
+ private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
+ private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
+ private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
+ private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
+
+ ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
+ ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
+ ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
+
+ ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
+
+ private static class MoveInfo {
+ public ViewHolder holder;
+ public int fromX, fromY, toX, toY;
+
+ MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ this.holder = holder;
+ this.fromX = fromX;
+ this.fromY = fromY;
+ this.toX = toX;
+ this.toY = toY;
+ }
+ }
+
+ private static class ChangeInfo {
+ public ViewHolder oldHolder, newHolder;
+ public int fromX, fromY, toX, toY;
+ private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
+ this.oldHolder = oldHolder;
+ this.newHolder = newHolder;
+ }
+
+ ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
+ int fromX, int fromY, int toX, int toY) {
+ this(oldHolder, newHolder);
+ this.fromX = fromX;
+ this.fromY = fromY;
+ this.toX = toX;
+ this.toY = toY;
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeInfo{"
+ + "oldHolder=" + oldHolder
+ + ", newHolder=" + newHolder
+ + ", fromX=" + fromX
+ + ", fromY=" + fromY
+ + ", toX=" + toX
+ + ", toY=" + toY
+ + '}';
+ }
+ }
+
+ @Override
+ public void runPendingAnimations() {
+ boolean removalsPending = !mPendingRemovals.isEmpty();
+ boolean movesPending = !mPendingMoves.isEmpty();
+ boolean changesPending = !mPendingChanges.isEmpty();
+ boolean additionsPending = !mPendingAdditions.isEmpty();
+ if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
+ // nothing to animate
+ return;
+ }
+ // First, remove stuff
+ for (ViewHolder holder : mPendingRemovals) {
+ animateRemoveImpl(holder);
+ }
+ mPendingRemovals.clear();
+ // Next, move stuff
+ if (movesPending) {
+ final ArrayList<MoveInfo> moves = new ArrayList<>();
+ moves.addAll(mPendingMoves);
+ mMovesList.add(moves);
+ mPendingMoves.clear();
+ Runnable mover = new Runnable() {
+ @Override
+ public void run() {
+ for (MoveInfo moveInfo : moves) {
+ animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
+ moveInfo.toX, moveInfo.toY);
+ }
+ moves.clear();
+ mMovesList.remove(moves);
+ }
+ };
+ if (removalsPending) {
+ View view = moves.get(0).holder.itemView;
+ view.postOnAnimationDelayed(mover, getRemoveDuration());
+ } else {
+ mover.run();
+ }
+ }
+ // Next, change stuff, to run in parallel with move animations
+ if (changesPending) {
+ final ArrayList<ChangeInfo> changes = new ArrayList<>();
+ changes.addAll(mPendingChanges);
+ mChangesList.add(changes);
+ mPendingChanges.clear();
+ Runnable changer = new Runnable() {
+ @Override
+ public void run() {
+ for (ChangeInfo change : changes) {
+ animateChangeImpl(change);
+ }
+ changes.clear();
+ mChangesList.remove(changes);
+ }
+ };
+ if (removalsPending) {
+ ViewHolder holder = changes.get(0).oldHolder;
+ holder.itemView.postOnAnimationDelayed(changer, getRemoveDuration());
+ } else {
+ changer.run();
+ }
+ }
+ // Next, add stuff
+ if (additionsPending) {
+ final ArrayList<ViewHolder> additions = new ArrayList<>();
+ additions.addAll(mPendingAdditions);
+ mAdditionsList.add(additions);
+ mPendingAdditions.clear();
+ Runnable adder = new Runnable() {
+ @Override
+ public void run() {
+ for (ViewHolder holder : additions) {
+ animateAddImpl(holder);
+ }
+ additions.clear();
+ mAdditionsList.remove(additions);
+ }
+ };
+ if (removalsPending || movesPending || changesPending) {
+ long removeDuration = removalsPending ? getRemoveDuration() : 0;
+ long moveDuration = movesPending ? getMoveDuration() : 0;
+ long changeDuration = changesPending ? getChangeDuration() : 0;
+ long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
+ View view = additions.get(0).itemView;
+ view.postOnAnimationDelayed(adder, totalDelay);
+ } else {
+ adder.run();
+ }
+ }
+ }
+
+ @Override
+ public boolean animateRemove(final ViewHolder holder) {
+ resetAnimation(holder);
+ mPendingRemovals.add(holder);
+ return true;
+ }
+
+ private void animateRemoveImpl(final ViewHolder holder) {
+ final View view = holder.itemView;
+ final ViewPropertyAnimator animation = view.animate();
+ mRemoveAnimations.add(holder);
+ animation.setDuration(getRemoveDuration()).alpha(0).setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchRemoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ view.setAlpha(1);
+ dispatchRemoveFinished(holder);
+ mRemoveAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+
+ @Override
+ public boolean animateAdd(final ViewHolder holder) {
+ resetAnimation(holder);
+ holder.itemView.setAlpha(0);
+ mPendingAdditions.add(holder);
+ return true;
+ }
+
+ void animateAddImpl(final ViewHolder holder) {
+ final View view = holder.itemView;
+ final ViewPropertyAnimator animation = view.animate();
+ mAddAnimations.add(holder);
+ animation.alpha(1).setDuration(getAddDuration())
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchAddStarting(holder);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ view.setAlpha(1);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ dispatchAddFinished(holder);
+ mAddAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+
+ @Override
+ public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
+ int toX, int toY) {
+ final View view = holder.itemView;
+ fromX += holder.itemView.getTranslationX();
+ fromY += holder.itemView.getTranslationY();
+ resetAnimation(holder);
+ int deltaX = toX - fromX;
+ int deltaY = toY - fromY;
+ if (deltaX == 0 && deltaY == 0) {
+ dispatchMoveFinished(holder);
+ return false;
+ }
+ if (deltaX != 0) {
+ view.setTranslationX(-deltaX);
+ }
+ if (deltaY != 0) {
+ view.setTranslationY(-deltaY);
+ }
+ mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
+ return true;
+ }
+
+ void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ final View view = holder.itemView;
+ final int deltaX = toX - fromX;
+ final int deltaY = toY - fromY;
+ if (deltaX != 0) {
+ view.animate().translationX(0);
+ }
+ if (deltaY != 0) {
+ view.animate().translationY(0);
+ }
+ // TODO: make EndActions end listeners instead, since end actions aren't called when
+ // vpas are canceled (and can't end them. why?)
+ // need listener functionality in VPACompat for this. Ick.
+ final ViewPropertyAnimator animation = view.animate();
+ mMoveAnimations.add(holder);
+ animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchMoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ if (deltaX != 0) {
+ view.setTranslationX(0);
+ }
+ if (deltaY != 0) {
+ view.setTranslationY(0);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ dispatchMoveFinished(holder);
+ mMoveAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+
+ @Override
+ public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
+ int fromX, int fromY, int toX, int toY) {
+ if (oldHolder == newHolder) {
+ // Don't know how to run change animations when the same view holder is re-used.
+ // run a move animation to handle position changes.
+ return animateMove(oldHolder, fromX, fromY, toX, toY);
+ }
+ final float prevTranslationX = oldHolder.itemView.getTranslationX();
+ final float prevTranslationY = oldHolder.itemView.getTranslationY();
+ final float prevAlpha = oldHolder.itemView.getAlpha();
+ resetAnimation(oldHolder);
+ int deltaX = (int) (toX - fromX - prevTranslationX);
+ int deltaY = (int) (toY - fromY - prevTranslationY);
+ // recover prev translation state after ending animation
+ oldHolder.itemView.setTranslationX(prevTranslationX);
+ oldHolder.itemView.setTranslationY(prevTranslationY);
+ oldHolder.itemView.setAlpha(prevAlpha);
+ if (newHolder != null) {
+ // carry over translation values
+ resetAnimation(newHolder);
+ newHolder.itemView.setTranslationX(-deltaX);
+ newHolder.itemView.setTranslationY(-deltaY);
+ newHolder.itemView.setAlpha(0);
+ }
+ mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
+ return true;
+ }
+
+ void animateChangeImpl(final ChangeInfo changeInfo) {
+ final ViewHolder holder = changeInfo.oldHolder;
+ final View view = holder == null ? null : holder.itemView;
+ final ViewHolder newHolder = changeInfo.newHolder;
+ final View newView = newHolder != null ? newHolder.itemView : null;
+ if (view != null) {
+ final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
+ getChangeDuration());
+ mChangeAnimations.add(changeInfo.oldHolder);
+ oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
+ oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
+ oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(changeInfo.oldHolder, true);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ oldViewAnim.setListener(null);
+ view.setAlpha(1);
+ view.setTranslationX(0);
+ view.setTranslationY(0);
+ dispatchChangeFinished(changeInfo.oldHolder, true);
+ mChangeAnimations.remove(changeInfo.oldHolder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+ if (newView != null) {
+ final ViewPropertyAnimator newViewAnimation = newView.animate();
+ mChangeAnimations.add(changeInfo.newHolder);
+ newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
+ .alpha(1).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(changeInfo.newHolder, false);
+ }
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ newViewAnimation.setListener(null);
+ newView.setAlpha(1);
+ newView.setTranslationX(0);
+ newView.setTranslationY(0);
+ dispatchChangeFinished(changeInfo.newHolder, false);
+ mChangeAnimations.remove(changeInfo.newHolder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+ }
+
+ private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
+ for (int i = infoList.size() - 1; i >= 0; i--) {
+ ChangeInfo changeInfo = infoList.get(i);
+ if (endChangeAnimationIfNecessary(changeInfo, item)) {
+ if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
+ infoList.remove(changeInfo);
+ }
+ }
+ }
+ }
+
+ private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
+ if (changeInfo.oldHolder != null) {
+ endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
+ }
+ if (changeInfo.newHolder != null) {
+ endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
+ }
+ }
+ private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
+ boolean oldItem = false;
+ if (changeInfo.newHolder == item) {
+ changeInfo.newHolder = null;
+ } else if (changeInfo.oldHolder == item) {
+ changeInfo.oldHolder = null;
+ oldItem = true;
+ } else {
+ return false;
+ }
+ item.itemView.setAlpha(1);
+ item.itemView.setTranslationX(0);
+ item.itemView.setTranslationY(0);
+ dispatchChangeFinished(item, oldItem);
+ return true;
+ }
+
+ @Override
+ public void endAnimation(ViewHolder item) {
+ final View view = item.itemView;
+ // this will trigger end callback which should set properties to their target values.
+ view.animate().cancel();
+ // TODO if some other animations are chained to end, how do we cancel them as well?
+ for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
+ MoveInfo moveInfo = mPendingMoves.get(i);
+ if (moveInfo.holder == item) {
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item);
+ mPendingMoves.remove(i);
+ }
+ }
+ endChangeAnimation(mPendingChanges, item);
+ if (mPendingRemovals.remove(item)) {
+ view.setAlpha(1);
+ dispatchRemoveFinished(item);
+ }
+ if (mPendingAdditions.remove(item)) {
+ view.setAlpha(1);
+ dispatchAddFinished(item);
+ }
+
+ for (int i = mChangesList.size() - 1; i >= 0; i--) {
+ ArrayList<ChangeInfo> changes = mChangesList.get(i);
+ endChangeAnimation(changes, item);
+ if (changes.isEmpty()) {
+ mChangesList.remove(i);
+ }
+ }
+ for (int i = mMovesList.size() - 1; i >= 0; i--) {
+ ArrayList<MoveInfo> moves = mMovesList.get(i);
+ for (int j = moves.size() - 1; j >= 0; j--) {
+ MoveInfo moveInfo = moves.get(j);
+ if (moveInfo.holder == item) {
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item);
+ moves.remove(j);
+ if (moves.isEmpty()) {
+ mMovesList.remove(i);
+ }
+ break;
+ }
+ }
+ }
+ for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
+ ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+ if (additions.remove(item)) {
+ view.setAlpha(1);
+ dispatchAddFinished(item);
+ if (additions.isEmpty()) {
+ mAdditionsList.remove(i);
+ }
+ }
+ }
+
+ // animations should be ended by the cancel above.
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mRemoveAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mRemoveAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mAddAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mAddAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mChangeAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mChangeAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mMoveAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mMoveAnimations list");
+ }
+ dispatchFinishedWhenDone();
+ }
+
+ private void resetAnimation(ViewHolder holder) {
+ if (sDefaultInterpolator == null) {
+ sDefaultInterpolator = new ValueAnimator().getInterpolator();
+ }
+ holder.itemView.animate().setInterpolator(sDefaultInterpolator);
+ endAnimation(holder);
+ }
+
+ @Override
+ public boolean isRunning() {
+ return (!mPendingAdditions.isEmpty()
+ || !mPendingChanges.isEmpty()
+ || !mPendingMoves.isEmpty()
+ || !mPendingRemovals.isEmpty()
+ || !mMoveAnimations.isEmpty()
+ || !mRemoveAnimations.isEmpty()
+ || !mAddAnimations.isEmpty()
+ || !mChangeAnimations.isEmpty()
+ || !mMovesList.isEmpty()
+ || !mAdditionsList.isEmpty()
+ || !mChangesList.isEmpty());
+ }
+
+ /**
+ * Check the state of currently pending and running animations. If there are none
+ * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
+ * listeners.
+ */
+ void dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished();
+ }
+ }
+
+ @Override
+ public void endAnimations() {
+ int count = mPendingMoves.size();
+ for (int i = count - 1; i >= 0; i--) {
+ MoveInfo item = mPendingMoves.get(i);
+ View view = item.holder.itemView;
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item.holder);
+ mPendingMoves.remove(i);
+ }
+ count = mPendingRemovals.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingRemovals.get(i);
+ dispatchRemoveFinished(item);
+ mPendingRemovals.remove(i);
+ }
+ count = mPendingAdditions.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingAdditions.get(i);
+ item.itemView.setAlpha(1);
+ dispatchAddFinished(item);
+ mPendingAdditions.remove(i);
+ }
+ count = mPendingChanges.size();
+ for (int i = count - 1; i >= 0; i--) {
+ endChangeAnimationIfNecessary(mPendingChanges.get(i));
+ }
+ mPendingChanges.clear();
+ if (!isRunning()) {
+ return;
+ }
+
+ int listCount = mMovesList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<MoveInfo> moves = mMovesList.get(i);
+ count = moves.size();
+ for (int j = count - 1; j >= 0; j--) {
+ MoveInfo moveInfo = moves.get(j);
+ ViewHolder item = moveInfo.holder;
+ View view = item.itemView;
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(moveInfo.holder);
+ moves.remove(j);
+ if (moves.isEmpty()) {
+ mMovesList.remove(moves);
+ }
+ }
+ }
+ listCount = mAdditionsList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+ count = additions.size();
+ for (int j = count - 1; j >= 0; j--) {
+ ViewHolder item = additions.get(j);
+ View view = item.itemView;
+ view.setAlpha(1);
+ dispatchAddFinished(item);
+ additions.remove(j);
+ if (additions.isEmpty()) {
+ mAdditionsList.remove(additions);
+ }
+ }
+ }
+ listCount = mChangesList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<ChangeInfo> changes = mChangesList.get(i);
+ count = changes.size();
+ for (int j = count - 1; j >= 0; j--) {
+ endChangeAnimationIfNecessary(changes.get(j));
+ if (changes.isEmpty()) {
+ mChangesList.remove(changes);
+ }
+ }
+ }
+
+ cancelAll(mRemoveAnimations);
+ cancelAll(mMoveAnimations);
+ cancelAll(mAddAnimations);
+ cancelAll(mChangeAnimations);
+
+ dispatchAnimationsFinished();
+ }
+
+ void cancelAll(List<ViewHolder> viewHolders) {
+ for (int i = viewHolders.size() - 1; i >= 0; i--) {
+ viewHolders.get(i).itemView.animate().cancel();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
+ * When this is the case:
+ * <ul>
+ * <li>If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both
+ * ViewHolder arguments will be the same instance.
+ * </li>
+ * <li>
+ * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)},
+ * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and
+ * run a move animation instead.
+ * </li>
+ * </ul>
+ */
+ @Override
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+ @NonNull List<Object> payloads) {
+ return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
+ }
+}
diff --git a/com/android/internal/widget/DialogTitle.java b/com/android/internal/widget/DialogTitle.java
new file mode 100644
index 0000000..0bfd684
--- /dev/null
+++ b/com/android/internal/widget/DialogTitle.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * 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.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.text.Layout;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+/**
+ * Used by dialogs to change the font size and number of lines to try to fit
+ * the text to the available space.
+ */
+public class DialogTitle extends TextView {
+
+ public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @UnsupportedAppUsage
+ public DialogTitle(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DialogTitle(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final Layout layout = getLayout();
+ if (layout != null) {
+ final int lineCount = layout.getLineCount();
+ if (lineCount > 0) {
+ final int ellipsisCount = layout.getEllipsisCount(lineCount - 1);
+ if (ellipsisCount > 0) {
+ setSingleLine(false);
+ setMaxLines(2);
+
+ final TypedArray a = mContext.obtainStyledAttributes(null,
+ android.R.styleable.TextAppearance, android.R.attr.textAppearanceMedium,
+ android.R.style.TextAppearance_Medium);
+ final int textSize = a.getDimensionPixelSize(
+ android.R.styleable.TextAppearance_textSize, 0);
+ if (textSize != 0) {
+ // textSize is already expressed in pixels
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+ }
+ a.recycle();
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+ }
+}
diff --git a/com/android/internal/widget/DialogViewAnimator.java b/com/android/internal/widget/DialogViewAnimator.java
new file mode 100644
index 0000000..bdfc1af
--- /dev/null
+++ b/com/android/internal/widget/DialogViewAnimator.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ViewAnimator;
+
+import java.util.ArrayList;
+
+/**
+ * ViewAnimator with a more reasonable handling of MATCH_PARENT.
+ */
+public class DialogViewAnimator extends ViewAnimator {
+ private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+ public DialogViewAnimator(Context context) {
+ super(context);
+ }
+
+ public DialogViewAnimator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final boolean measureMatchParentChildren =
+ MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+ MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ // First measure all children and record maximum dimensions where the
+ // spec isn't MATCH_PARENT.
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (getMeasureAllChildren() || child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final boolean matchWidth = lp.width == LayoutParams.MATCH_PARENT;
+ final boolean matchHeight = lp.height == LayoutParams.MATCH_PARENT;
+ if (measureMatchParentChildren && (matchWidth || matchHeight)) {
+ mMatchParentChildren.add(child);
+ }
+
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+
+ // Measured dimensions only count against the maximum
+ // dimensions if they're not MATCH_PARENT.
+ int state = 0;
+
+ if (measureMatchParentChildren && !matchWidth) {
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth()
+ + lp.leftMargin + lp.rightMargin);
+ state |= child.getMeasuredWidthAndState() & MEASURED_STATE_MASK;
+ }
+
+ if (measureMatchParentChildren && !matchHeight) {
+ maxHeight = Math.max(maxHeight, child.getMeasuredHeight()
+ + lp.topMargin + lp.bottomMargin);
+ state |= (child.getMeasuredHeightAndState() >> MEASURED_HEIGHT_STATE_SHIFT)
+ & (MEASURED_STATE_MASK >> MEASURED_HEIGHT_STATE_SHIFT);
+ }
+
+ childState = combineMeasuredStates(childState, state);
+ }
+ }
+
+ // Account for padding too.
+ maxWidth += getPaddingLeft() + getPaddingRight();
+ maxHeight += getPaddingTop() + getPaddingBottom();
+
+ // Check against our minimum height and width.
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ // Check against our foreground's minimum height and width.
+ final Drawable drawable = getForeground();
+ if (drawable != null) {
+ maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+ maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+ }
+
+ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+ // Measure remaining MATCH_PARENT children again using real dimensions.
+ final int matchCount = mMatchParentChildren.size();
+ for (int i = 0; i < matchCount; i++) {
+ final View child = mMatchParentChildren.get(i);
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec;
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight()
+ - lp.leftMargin - lp.rightMargin,
+ MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin,
+ lp.width);
+ }
+
+ final int childHeightMeasureSpec;
+ if (lp.height == LayoutParams.MATCH_PARENT) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom()
+ - lp.topMargin - lp.bottomMargin,
+ MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin,
+ lp.height);
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ mMatchParentChildren.clear();
+ }
+}
diff --git a/com/android/internal/widget/DrawableHolder.java b/com/android/internal/widget/DrawableHolder.java
new file mode 100644
index 0000000..947e0f3
--- /dev/null
+++ b/com/android/internal/widget/DrawableHolder.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import java.util.ArrayList;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.animation.Animator.AnimatorListener;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+import android.view.animation.DecelerateInterpolator;
+
+/**
+ * This class is a container for a Drawable with multiple animated properties.
+ *
+ */
+public class DrawableHolder implements AnimatorListener {
+ public static final DecelerateInterpolator EASE_OUT_INTERPOLATOR = new DecelerateInterpolator();
+ private static final String TAG = "DrawableHolder";
+ private static final boolean DBG = false;
+ private float mX = 0.0f;
+ private float mY = 0.0f;
+ private float mScaleX = 1.0f;
+ private float mScaleY = 1.0f;
+ private BitmapDrawable mDrawable;
+ private float mAlpha = 1f;
+ private ArrayList<ObjectAnimator> mAnimators = new ArrayList<ObjectAnimator>();
+ private ArrayList<ObjectAnimator> mNeedToStart = new ArrayList<ObjectAnimator>();
+
+ public DrawableHolder(BitmapDrawable drawable) {
+ this(drawable, 0.0f, 0.0f);
+ }
+
+ public DrawableHolder(BitmapDrawable drawable, float x, float y) {
+ mDrawable = drawable;
+ mX = x;
+ mY = y;
+ mDrawable.getPaint().setAntiAlias(true); // Force AA
+ mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
+ }
+
+ /**
+ *
+ * Adds an animation that interpolates given property from its current value
+ * to the given value.
+ *
+ * @param duration the duration, in ms.
+ * @param delay the delay to start the animation, in ms.
+ * @param property the property to animate
+ * @param toValue the target value
+ * @param replace if true, replace the current animation with this one.
+ */
+ public ObjectAnimator addAnimTo(long duration, long delay,
+ String property, float toValue, boolean replace) {
+
+ if (replace) removeAnimationFor(property);
+
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, property, toValue);
+ anim.setDuration(duration);
+ anim.setStartDelay(delay);
+ anim.setInterpolator(EASE_OUT_INTERPOLATOR);
+ this.addAnimation(anim, replace);
+ if (DBG) Log.v(TAG, "animationCount = " + mAnimators.size());
+ return anim;
+ }
+
+ /**
+ * Stops all animations for the given property and removes it from the list.
+ *
+ * @param property
+ */
+ public void removeAnimationFor(String property) {
+ ArrayList<ObjectAnimator> removalList = (ArrayList<ObjectAnimator>)mAnimators.clone();
+ for (ObjectAnimator currentAnim : removalList) {
+ if (property.equals(currentAnim.getPropertyName())) {
+ currentAnim.cancel();
+ }
+ }
+ }
+
+ /**
+ * Stops all animations and removes them from the list.
+ */
+ public void clearAnimations() {
+ for (ObjectAnimator currentAnim : mAnimators) {
+ currentAnim.cancel();
+ }
+ mAnimators.clear();
+ }
+
+ /**
+ * Adds the given animation to the list of animations for this object.
+ *
+ * @param anim
+ * @param overwrite
+ * @return
+ */
+ private DrawableHolder addAnimation(ObjectAnimator anim, boolean overwrite) {
+ if (anim != null)
+ mAnimators.add(anim);
+ mNeedToStart.add(anim);
+ return this;
+ }
+
+ /**
+ * Draw this object to the canvas using the properties defined in this class.
+ *
+ * @param canvas canvas to draw into
+ */
+ public void draw(Canvas canvas) {
+ final float threshold = 1.0f / 256.0f; // contribution less than 1 LSB of RGB byte
+ if (mAlpha <= threshold) // don't bother if it won't show up
+ return;
+ canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ canvas.translate(mX, mY);
+ canvas.scale(mScaleX, mScaleY);
+ canvas.translate(-0.5f*getWidth(), -0.5f*getHeight());
+ mDrawable.setAlpha((int) Math.round(mAlpha * 255f));
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+
+ /**
+ * Starts all animations added since the last call to this function. Used to synchronize
+ * animations.
+ *
+ * @param listener an optional listener to add to the animations. Typically used to know when
+ * to invalidate the surface these are being drawn to.
+ */
+ public void startAnimations(ValueAnimator.AnimatorUpdateListener listener) {
+ for (int i = 0; i < mNeedToStart.size(); i++) {
+ ObjectAnimator anim = mNeedToStart.get(i);
+ anim.addUpdateListener(listener);
+ anim.addListener(this);
+ anim.start();
+ }
+ mNeedToStart.clear();
+ }
+
+
+ public void setX(float value) {
+ mX = value;
+ }
+
+ public void setY(float value) {
+ mY = value;
+ }
+
+ public void setScaleX(float value) {
+ mScaleX = value;
+ }
+
+ public void setScaleY(float value) {
+ mScaleY = value;
+ }
+
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ }
+
+ public float getX() {
+ return mX;
+ }
+
+ public float getY() {
+ return mY;
+ }
+
+ public float getScaleX() {
+ return mScaleX;
+ }
+
+ public float getScaleY() {
+ return mScaleY;
+ }
+
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ public BitmapDrawable getDrawable() {
+ return mDrawable;
+ }
+
+ public int getWidth() {
+ return mDrawable.getIntrinsicWidth();
+ }
+
+ public int getHeight() {
+ return mDrawable.getIntrinsicHeight();
+ }
+
+ public void onAnimationCancel(Animator animation) {
+
+ }
+
+ public void onAnimationEnd(Animator animation) {
+ mAnimators.remove(animation);
+ }
+
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+
+ public void onAnimationStart(Animator animation) {
+
+ }
+}
diff --git a/com/android/internal/widget/EditableInputConnection.java b/com/android/internal/widget/EditableInputConnection.java
new file mode 100644
index 0000000..ff3543c
--- /dev/null
+++ b/com/android/internal/widget/EditableInputConnection.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2007-2008 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.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.method.KeyListener;
+import android.util.Log;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+public class EditableInputConnection extends BaseInputConnection {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "EditableInputConnection";
+
+ private final TextView mTextView;
+
+ // Keeps track of nested begin/end batch edit to ensure this connection always has a
+ // balanced impact on its associated TextView.
+ // A negative value means that this connection has been finished by the InputMethodManager.
+ private int mBatchEditNesting;
+
+ @UnsupportedAppUsage
+ public EditableInputConnection(TextView textview) {
+ super(textview, true);
+ mTextView = textview;
+ }
+
+ @Override
+ public Editable getEditable() {
+ TextView tv = mTextView;
+ if (tv != null) {
+ return tv.getEditableText();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean beginBatchEdit() {
+ synchronized(this) {
+ if (mBatchEditNesting >= 0) {
+ mTextView.beginBatchEdit();
+ mBatchEditNesting++;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean endBatchEdit() {
+ synchronized(this) {
+ if (mBatchEditNesting > 0) {
+ // When the connection is reset by the InputMethodManager and reportFinish
+ // is called, some endBatchEdit calls may still be asynchronously received from the
+ // IME. Do not take these into account, thus ensuring that this IC's final
+ // contribution to mTextView's nested batch edit count is zero.
+ mTextView.endBatchEdit();
+ mBatchEditNesting--;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void closeConnection() {
+ super.closeConnection();
+ synchronized(this) {
+ while (mBatchEditNesting > 0) {
+ endBatchEdit();
+ }
+ // Will prevent any further calls to begin or endBatchEdit
+ mBatchEditNesting = -1;
+ }
+ }
+
+ @Override
+ public boolean clearMetaKeyStates(int states) {
+ final Editable content = getEditable();
+ if (content == null) return false;
+ KeyListener kl = mTextView.getKeyListener();
+ if (kl != null) {
+ try {
+ kl.clearMetaKeyState(mTextView, content, states);
+ } catch (AbstractMethodError e) {
+ // This is an old listener that doesn't implement the
+ // new method.
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean commitCompletion(CompletionInfo text) {
+ if (DEBUG) Log.v(TAG, "commitCompletion " + text);
+ mTextView.beginBatchEdit();
+ mTextView.onCommitCompletion(text);
+ mTextView.endBatchEdit();
+ return true;
+ }
+
+ /**
+ * Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
+ */
+ @Override
+ public boolean commitCorrection(CorrectionInfo correctionInfo) {
+ if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
+ mTextView.beginBatchEdit();
+ mTextView.onCommitCorrection(correctionInfo);
+ mTextView.endBatchEdit();
+ return true;
+ }
+
+ @Override
+ public boolean performEditorAction(int actionCode) {
+ if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode);
+ mTextView.onEditorAction(actionCode);
+ return true;
+ }
+
+ @Override
+ public boolean performContextMenuAction(int id) {
+ if (DEBUG) Log.v(TAG, "performContextMenuAction " + id);
+ mTextView.beginBatchEdit();
+ mTextView.onTextContextMenuItem(id);
+ mTextView.endBatchEdit();
+ return true;
+ }
+
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+ if (mTextView != null) {
+ ExtractedText et = new ExtractedText();
+ if (mTextView.extractText(request, et)) {
+ if ((flags&GET_EXTRACTED_TEXT_MONITOR) != 0) {
+ mTextView.setExtracting(request);
+ }
+ return et;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean performPrivateCommand(String action, Bundle data) {
+ mTextView.onPrivateIMECommand(action, data);
+ return true;
+ }
+
+ @Override
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ if (mTextView == null) {
+ return super.commitText(text, newCursorPosition);
+ }
+ mTextView.resetErrorChangedFlag();
+ boolean success = super.commitText(text, newCursorPosition);
+ mTextView.hideErrorIfUnchanged();
+
+ return success;
+ }
+
+ @Override
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+ if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
+
+ // It is possible that any other bit is used as a valid flag in a future release.
+ // We should reject the entire request in such a case.
+ final int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE |
+ InputConnection.CURSOR_UPDATE_MONITOR;
+ final int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK;
+ if (unknownFlags != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." +
+ " cursorUpdateMode=" + cursorUpdateMode +
+ " unknownFlags=" + unknownFlags);
+ }
+ return false;
+ }
+
+ if (mIMM == null) {
+ // In this case, TYPE_CURSOR_ANCHOR_INFO is not handled.
+ // TODO: Return some notification code rather than false to indicate method that
+ // CursorAnchorInfo is temporarily unavailable.
+ return false;
+ }
+ mIMM.setUpdateCursorAnchorInfoMode(cursorUpdateMode);
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
+ if (mTextView == null) {
+ // In this case, FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is silently ignored.
+ // TODO: Return some notification code for the input method that indicates
+ // FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is ignored.
+ } else if (mTextView.isInLayout()) {
+ // In this case, the view hierarchy is currently undergoing a layout pass.
+ // IMM#updateCursorAnchorInfo is supposed to be called soon after the layout
+ // pass is finished.
+ } else {
+ // This will schedule a layout pass of the view tree, and the layout event
+ // eventually triggers IMM#updateCursorAnchorInfo.
+ mTextView.requestLayout();
+ }
+ }
+ return true;
+ }
+}
diff --git a/com/android/internal/widget/EmphasizedNotificationButton.java b/com/android/internal/widget/EmphasizedNotificationButton.java
new file mode 100644
index 0000000..5213746
--- /dev/null
+++ b/com/android/internal/widget/EmphasizedNotificationButton.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.DrawableWrapper;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.graphics.drawable.RippleDrawable;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.widget.Button;
+import android.widget.RemoteViews;
+
+/**
+ * A button implementation for the emphasized notification style.
+ *
+ * @hide
+ */
[email protected]
+public class EmphasizedNotificationButton extends Button {
+ private final RippleDrawable mRipple;
+ private final int mStrokeWidth;
+ private final int mStrokeColor;
+
+ public EmphasizedNotificationButton(Context context) {
+ this(context, null);
+ }
+
+ public EmphasizedNotificationButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public EmphasizedNotificationButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public EmphasizedNotificationButton(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ DrawableWrapper background = (DrawableWrapper) getBackground().mutate();
+ mRipple = (RippleDrawable) background.getDrawable();
+ mStrokeWidth = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.emphasized_button_stroke_width);
+ mStrokeColor = getContext().getColor(com.android.internal.R.color.material_grey_300);
+ mRipple.mutate();
+ }
+
+ @RemotableViewMethod
+ public void setRippleColor(ColorStateList color) {
+ mRipple.setColor(color);
+ invalidate();
+ }
+
+ @RemotableViewMethod
+ public void setButtonBackground(ColorStateList color) {
+ GradientDrawable inner = (GradientDrawable) mRipple.getDrawable(0);
+ inner.setColor(color);
+ invalidate();
+ }
+
+ @RemotableViewMethod
+ public void setHasStroke(boolean hasStroke) {
+ GradientDrawable inner = (GradientDrawable) mRipple.getDrawable(0);
+ inner.setStroke(hasStroke ? mStrokeWidth : 0, mStrokeColor);
+ invalidate();
+ }
+}
diff --git a/com/android/internal/widget/ExploreByTouchHelper.java b/com/android/internal/widget/ExploreByTouchHelper.java
new file mode 100644
index 0000000..b54e5fb
--- /dev/null
+++ b/com/android/internal/widget/ExploreByTouchHelper.java
@@ -0,0 +1,828 @@
+/*
+ * Copyright (C) 2013 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.IntArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+/**
+ * ExploreByTouchHelper is a utility class for implementing accessibility
+ * support in custom {@link android.view.View}s that represent a collection of View-like
+ * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and
+ * simplifies many aspects of providing information to accessibility services
+ * and managing accessibility focus. This class does not currently support
+ * hierarchies of logical items.
+ * <p>
+ * This should be applied to the parent view using
+ * {@link android.view.View#setAccessibilityDelegate}:
+ *
+ * <pre>
+ * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback);
+ * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
+ * </pre>
+ */
+public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
+ /** Virtual node identifier value for invalid nodes. */
+ public static final int INVALID_ID = Integer.MIN_VALUE;
+
+ /** Virtual node identifier value for the host view's node. */
+ public static final int HOST_ID = View.NO_ID;
+
+ /** Default class name used for virtual views. */
+ private static final String DEFAULT_CLASS_NAME = View.class.getName();
+
+ /** Default bounds used to determine if the client didn't set any. */
+ private static final Rect INVALID_PARENT_BOUNDS = new Rect(
+ Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
+
+ // Lazily-created temporary data structures used when creating nodes.
+ private Rect mTempScreenRect;
+ private Rect mTempParentRect;
+ private int[] mTempGlobalRect;
+
+ /** Lazily-created temporary data structure used to compute visibility. */
+ private Rect mTempVisibleRect;
+
+ /** Lazily-created temporary data structure used to obtain child IDs. */
+ private IntArray mTempArray;
+
+ /** System accessibility manager, used to check state and send events. */
+ private final AccessibilityManager mManager;
+
+ /** View whose internal structure is exposed through this helper. */
+ private final View mView;
+
+ /** Context of the host view. **/
+ private final Context mContext;
+
+ /** Node provider that handles creating nodes and performing actions. */
+ private ExploreByTouchNodeProvider mNodeProvider;
+
+ /** Virtual view id for the currently focused logical item. */
+ private int mFocusedVirtualViewId = INVALID_ID;
+
+ /** Virtual view id for the currently hovered logical item. */
+ private int mHoveredVirtualViewId = INVALID_ID;
+
+ /**
+ * Factory method to create a new {@link ExploreByTouchHelper}.
+ *
+ * @param forView View whose logical children are exposed by this helper.
+ */
+ public ExploreByTouchHelper(View forView) {
+ if (forView == null) {
+ throw new IllegalArgumentException("View may not be null");
+ }
+
+ mView = forView;
+ mContext = forView.getContext();
+ mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ }
+
+ /**
+ * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper.
+ *
+ * @param host View whose logical children are exposed by this helper.
+ * @return The accessibility node provider for this helper.
+ */
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ if (mNodeProvider == null) {
+ mNodeProvider = new ExploreByTouchNodeProvider();
+ }
+ return mNodeProvider;
+ }
+
+ /**
+ * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when
+ * the Explore by Touch feature is enabled.
+ * <p>
+ * This method should be called by overriding
+ * {@link View#dispatchHoverEvent}:
+ *
+ * <pre>@Override
+ * public boolean dispatchHoverEvent(MotionEvent event) {
+ * if (mHelper.dispatchHoverEvent(this, event) {
+ * return true;
+ * }
+ * return super.dispatchHoverEvent(event);
+ * }
+ * </pre>
+ *
+ * @param event The hover event to dispatch to the virtual view hierarchy.
+ * @return Whether the hover event was handled.
+ */
+ public boolean dispatchHoverEvent(MotionEvent event) {
+ if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_HOVER_MOVE:
+ case MotionEvent.ACTION_HOVER_ENTER:
+ final int virtualViewId = getVirtualViewAt(event.getX(), event.getY());
+ updateHoveredVirtualView(virtualViewId);
+ return (virtualViewId != INVALID_ID);
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (mHoveredVirtualViewId != INVALID_ID) {
+ updateHoveredVirtualView(INVALID_ID);
+ return true;
+ }
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Populates an event of the specified type with information about an item
+ * and attempts to send it up through the view hierarchy.
+ * <p>
+ * You should call this method after performing a user action that normally
+ * fires an accessibility event, such as clicking on an item.
+ *
+ * <pre>public void performItemClick(T item) {
+ * ...
+ * sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED);
+ * }
+ * </pre>
+ *
+ * @param virtualViewId The virtual view id for which to send an event.
+ * @param eventType The type of event to send.
+ * @return true if the event was sent successfully.
+ */
+ public boolean sendEventForVirtualView(int virtualViewId, int eventType) {
+ if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) {
+ return false;
+ }
+
+ final ViewParent parent = mView.getParent();
+ if (parent == null) {
+ return false;
+ }
+
+ final AccessibilityEvent event = createEvent(virtualViewId, eventType);
+ return parent.requestSendAccessibilityEvent(mView, event);
+ }
+
+ /**
+ * Notifies the accessibility framework that the properties of the parent
+ * view have changed.
+ * <p>
+ * You <b>must</b> call this method after adding or removing items from the
+ * parent view.
+ */
+ public void invalidateRoot() {
+ invalidateVirtualView(HOST_ID, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+ }
+
+ /**
+ * Notifies the accessibility framework that the properties of a particular
+ * item have changed.
+ * <p>
+ * You <b>must</b> call this method after changing any of the properties set
+ * in {@link #onPopulateNodeForVirtualView}.
+ *
+ * @param virtualViewId The virtual view id to invalidate, or
+ * {@link #HOST_ID} to invalidate the root view.
+ * @see #invalidateVirtualView(int, int)
+ */
+ public void invalidateVirtualView(int virtualViewId) {
+ invalidateVirtualView(virtualViewId,
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ /**
+ * Notifies the accessibility framework that the properties of a particular
+ * item have changed.
+ * <p>
+ * You <b>must</b> call this method after changing any of the properties set
+ * in {@link #onPopulateNodeForVirtualView}.
+ *
+ * @param virtualViewId The virtual view id to invalidate, or
+ * {@link #HOST_ID} to invalidate the root view.
+ * @param changeTypes The bit mask of change types. May be {@code 0} for the
+ * default (undefined) change type or one or more of:
+ * <ul>
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_STATE_DESCRIPTION}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
+ * </ul>
+ */
+ public void invalidateVirtualView(int virtualViewId, int changeTypes) {
+ if (virtualViewId != INVALID_ID && mManager.isEnabled()) {
+ final ViewParent parent = mView.getParent();
+ if (parent != null) {
+ final AccessibilityEvent event = createEvent(virtualViewId,
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(changeTypes);
+ parent.requestSendAccessibilityEvent(mView, event);
+ }
+ }
+ }
+
+ /**
+ * Returns the virtual view id for the currently focused item,
+ *
+ * @return A virtual view id, or {@link #INVALID_ID} if no item is
+ * currently focused.
+ */
+ public int getFocusedVirtualView() {
+ return mFocusedVirtualViewId;
+ }
+
+ /**
+ * Sets the currently hovered item, sending hover accessibility events as
+ * necessary to maintain the correct state.
+ *
+ * @param virtualViewId The virtual view id for the item currently being
+ * hovered, or {@link #INVALID_ID} if no item is hovered within
+ * the parent view.
+ */
+ private void updateHoveredVirtualView(int virtualViewId) {
+ if (mHoveredVirtualViewId == virtualViewId) {
+ return;
+ }
+
+ final int previousVirtualViewId = mHoveredVirtualViewId;
+ mHoveredVirtualViewId = virtualViewId;
+
+ // Stay consistent with framework behavior by sending ENTER/EXIT pairs
+ // in reverse order. This is accurate as of API 18.
+ sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ }
+
+ /**
+ * Constructs and returns an {@link AccessibilityEvent} for the specified
+ * virtual view id, which includes the host view ({@link #HOST_ID}).
+ *
+ * @param virtualViewId The virtual view id for the item for which to
+ * construct an event.
+ * @param eventType The type of event to construct.
+ * @return An {@link AccessibilityEvent} populated with information about
+ * the specified item.
+ */
+ private AccessibilityEvent createEvent(int virtualViewId, int eventType) {
+ switch (virtualViewId) {
+ case HOST_ID:
+ return createEventForHost(eventType);
+ default:
+ return createEventForChild(virtualViewId, eventType);
+ }
+ }
+
+ /**
+ * Constructs and returns an {@link AccessibilityEvent} for the host node.
+ *
+ * @param eventType The type of event to construct.
+ * @return An {@link AccessibilityEvent} populated with information about
+ * the specified item.
+ */
+ private AccessibilityEvent createEventForHost(int eventType) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ mView.onInitializeAccessibilityEvent(event);
+
+ // Allow the client to populate the event.
+ onPopulateEventForHost(event);
+
+ return event;
+ }
+
+ /**
+ * Constructs and returns an {@link AccessibilityEvent} populated with
+ * information about the specified item.
+ *
+ * @param virtualViewId The virtual view id for the item for which to
+ * construct an event.
+ * @param eventType The type of event to construct.
+ * @return An {@link AccessibilityEvent} populated with information about
+ * the specified item.
+ */
+ private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setEnabled(true);
+ event.setClassName(DEFAULT_CLASS_NAME);
+
+ // Allow the client to populate the event.
+ onPopulateEventForVirtualView(virtualViewId, event);
+
+ // Make sure the developer is following the rules.
+ if (event.getText().isEmpty() && (event.getContentDescription() == null)) {
+ throw new RuntimeException("Callbacks must add text or a content description in "
+ + "populateEventForVirtualViewId()");
+ }
+
+ // Don't allow the client to override these properties.
+ event.setPackageName(mView.getContext().getPackageName());
+ event.setSource(mView, virtualViewId);
+
+ return event;
+ }
+
+ /**
+ * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the
+ * specified virtual view id, which includes the host view
+ * ({@link #HOST_ID}).
+ *
+ * @param virtualViewId The virtual view id for the item for which to
+ * construct a node.
+ * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information
+ * about the specified item.
+ */
+ private AccessibilityNodeInfo createNode(int virtualViewId) {
+ switch (virtualViewId) {
+ case HOST_ID:
+ return createNodeForHost();
+ default:
+ return createNodeForChild(virtualViewId);
+ }
+ }
+
+ /**
+ * Constructs and returns an {@link AccessibilityNodeInfo} for the
+ * host view populated with its virtual descendants.
+ *
+ * @return An {@link AccessibilityNodeInfo} for the parent node.
+ */
+ private AccessibilityNodeInfo createNodeForHost() {
+ final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView);
+ mView.onInitializeAccessibilityNodeInfo(node);
+ final int realNodeCount = node.getChildCount();
+
+ // Allow the client to populate the host node.
+ onPopulateNodeForHost(node);
+
+ // Add the virtual descendants.
+ if (mTempArray == null) {
+ mTempArray = new IntArray();
+ } else {
+ mTempArray.clear();
+ }
+ final IntArray virtualViewIds = mTempArray;
+ getVisibleVirtualViews(virtualViewIds);
+ if (realNodeCount > 0 && virtualViewIds.size() > 0) {
+ throw new RuntimeException("Views cannot have both real and virtual children");
+ }
+
+ final int N = virtualViewIds.size();
+ for (int i = 0; i < N; i++) {
+ node.addChild(mView, virtualViewIds.get(i));
+ }
+
+ return node;
+ }
+
+ /**
+ * Constructs and returns an {@link AccessibilityNodeInfo} for the
+ * specified item. Automatically manages accessibility focus actions.
+ * <p>
+ * Allows the implementing class to specify most node properties, but
+ * overrides the following:
+ * <ul>
+ * <li>{@link AccessibilityNodeInfo#setPackageName}
+ * <li>{@link AccessibilityNodeInfo#setClassName}
+ * <li>{@link AccessibilityNodeInfo#setParent(View)}
+ * <li>{@link AccessibilityNodeInfo#setSource(View, int)}
+ * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
+ * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)}
+ * </ul>
+ * <p>
+ * Uses the bounds of the parent view and the parent-relative bounding
+ * rectangle specified by
+ * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically
+ * update the following properties:
+ * <ul>
+ * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
+ * <li>{@link AccessibilityNodeInfo#setBoundsInParent}
+ * </ul>
+ *
+ * @param virtualViewId The virtual view id for item for which to construct
+ * a node.
+ * @return An {@link AccessibilityNodeInfo} for the specified item.
+ */
+ private AccessibilityNodeInfo createNodeForChild(int virtualViewId) {
+ ensureTempRects();
+ final Rect tempParentRect = mTempParentRect;
+ final int[] tempGlobalRect = mTempGlobalRect;
+ final Rect tempScreenRect = mTempScreenRect;
+
+ final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+
+ // Ensure the client has good defaults.
+ node.setEnabled(true);
+ node.setClassName(DEFAULT_CLASS_NAME);
+ node.setBoundsInParent(INVALID_PARENT_BOUNDS);
+
+ // Allow the client to populate the node.
+ onPopulateNodeForVirtualView(virtualViewId, node);
+
+ // Make sure the developer is following the rules.
+ if ((node.getText() == null) && (node.getContentDescription() == null)) {
+ throw new RuntimeException("Callbacks must add text or a content description in "
+ + "populateNodeForVirtualViewId()");
+ }
+
+ node.getBoundsInParent(tempParentRect);
+ if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) {
+ throw new RuntimeException("Callbacks must set parent bounds in "
+ + "populateNodeForVirtualViewId()");
+ }
+
+ final int actions = node.getActions();
+ if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) {
+ throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in "
+ + "populateNodeForVirtualViewId()");
+ }
+ if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) {
+ throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in "
+ + "populateNodeForVirtualViewId()");
+ }
+
+ // Don't allow the client to override these properties.
+ node.setPackageName(mView.getContext().getPackageName());
+ node.setSource(mView, virtualViewId);
+ node.setParent(mView);
+
+ // Manage internal accessibility focus state.
+ if (mFocusedVirtualViewId == virtualViewId) {
+ node.setAccessibilityFocused(true);
+ node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ } else {
+ node.setAccessibilityFocused(false);
+ node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ // Set the visibility based on the parent bound.
+ if (intersectVisibleToUser(tempParentRect)) {
+ node.setVisibleToUser(true);
+ node.setBoundsInParent(tempParentRect);
+ }
+
+ // Calculate screen-relative bound.
+ mView.getLocationOnScreen(tempGlobalRect);
+ final int offsetX = tempGlobalRect[0];
+ final int offsetY = tempGlobalRect[1];
+ tempScreenRect.set(tempParentRect);
+ tempScreenRect.offset(offsetX, offsetY);
+ node.setBoundsInScreen(tempScreenRect);
+
+ return node;
+ }
+
+ private void ensureTempRects() {
+ mTempGlobalRect = new int[2];
+ mTempParentRect = new Rect();
+ mTempScreenRect = new Rect();
+ }
+
+ private boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ switch (virtualViewId) {
+ case HOST_ID:
+ return performActionForHost(action, arguments);
+ default:
+ return performActionForChild(virtualViewId, action, arguments);
+ }
+ }
+
+ private boolean performActionForHost(int action, Bundle arguments) {
+ return mView.performAccessibilityAction(action, arguments);
+ }
+
+ private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+ return manageFocusForChild(virtualViewId, action);
+ default:
+ return onPerformActionForVirtualView(virtualViewId, action, arguments);
+ }
+ }
+
+ private boolean manageFocusForChild(int virtualViewId, int action) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+ return requestAccessibilityFocus(virtualViewId);
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+ return clearAccessibilityFocus(virtualViewId);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Computes whether the specified {@link Rect} intersects with the visible
+ * portion of its parent {@link View}. Modifies {@code localRect} to contain
+ * only the visible portion.
+ *
+ * @param localRect A rectangle in local (parent) coordinates.
+ * @return Whether the specified {@link Rect} is visible on the screen.
+ */
+ private boolean intersectVisibleToUser(Rect localRect) {
+ // Missing or empty bounds mean this view is not visible.
+ if ((localRect == null) || localRect.isEmpty()) {
+ return false;
+ }
+
+ // Attached to invisible window means this view is not visible.
+ if (mView.getWindowVisibility() != View.VISIBLE) {
+ return false;
+ }
+
+ // An invisible predecessor means that this view is not visible.
+ ViewParent viewParent = mView.getParent();
+ while (viewParent instanceof View) {
+ final View view = (View) viewParent;
+ if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) {
+ return false;
+ }
+ viewParent = view.getParent();
+ }
+
+ // A null parent implies the view is not visible.
+ if (viewParent == null) {
+ return false;
+ }
+
+ // If no portion of the parent is visible, this view is not visible.
+ if (mTempVisibleRect == null) {
+ mTempVisibleRect = new Rect();
+ }
+ final Rect tempVisibleRect = mTempVisibleRect;
+ if (!mView.getLocalVisibleRect(tempVisibleRect)) {
+ return false;
+ }
+
+ // Check if the view intersects the visible portion of the parent.
+ return localRect.intersect(tempVisibleRect);
+ }
+
+ /**
+ * Returns whether this virtual view is accessibility focused.
+ *
+ * @return True if the view is accessibility focused.
+ */
+ private boolean isAccessibilityFocused(int virtualViewId) {
+ return (mFocusedVirtualViewId == virtualViewId);
+ }
+
+ /**
+ * Attempts to give accessibility focus to a virtual view.
+ * <p>
+ * A virtual view will not actually take focus if
+ * {@link AccessibilityManager#isEnabled()} returns false,
+ * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
+ * or the view already has accessibility focus.
+ *
+ * @param virtualViewId The id of the virtual view on which to place
+ * accessibility focus.
+ * @return Whether this virtual view actually took accessibility focus.
+ */
+ private boolean requestAccessibilityFocus(int virtualViewId) {
+ final AccessibilityManager accessibilityManager =
+ (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+ if (!mManager.isEnabled()
+ || !accessibilityManager.isTouchExplorationEnabled()) {
+ return false;
+ }
+ // TODO: Check virtual view visibility.
+ if (!isAccessibilityFocused(virtualViewId)) {
+ // Clear focus from the previously focused view, if applicable.
+ if (mFocusedVirtualViewId != INVALID_ID) {
+ sendEventForVirtualView(mFocusedVirtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ }
+
+ // Set focus on the new view.
+ mFocusedVirtualViewId = virtualViewId;
+
+ // TODO: Only invalidate virtual view bounds.
+ mView.invalidate();
+ sendEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to clear accessibility focus from a virtual view.
+ *
+ * @param virtualViewId The id of the virtual view from which to clear
+ * accessibility focus.
+ * @return Whether this virtual view actually cleared accessibility focus.
+ */
+ private boolean clearAccessibilityFocus(int virtualViewId) {
+ if (isAccessibilityFocused(virtualViewId)) {
+ mFocusedVirtualViewId = INVALID_ID;
+ mView.invalidate();
+ sendEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Provides a mapping between view-relative coordinates and logical
+ * items.
+ *
+ * @param x The view-relative x coordinate
+ * @param y The view-relative y coordinate
+ * @return virtual view identifier for the logical item under
+ * coordinates (x,y)
+ */
+ protected abstract int getVirtualViewAt(float x, float y);
+
+ /**
+ * Populates a list with the view's visible items. The ordering of items
+ * within {@code virtualViewIds} specifies order of accessibility focus
+ * traversal.
+ *
+ * @param virtualViewIds The list to populate with visible items
+ */
+ protected abstract void getVisibleVirtualViews(IntArray virtualViewIds);
+
+ /**
+ * Populates an {@link AccessibilityEvent} with information about the
+ * specified item.
+ * <p>
+ * Implementations <b>must</b> populate the following required fields:
+ * <ul>
+ * <li>event text, see {@link AccessibilityEvent#getText} or
+ * {@link AccessibilityEvent#setContentDescription}
+ * </ul>
+ * <p>
+ * The helper class automatically populates the following fields with
+ * default values, but implementations may optionally override them:
+ * <ul>
+ * <li>item class name, set to android.view.View, see
+ * {@link AccessibilityEvent#setClassName}
+ * </ul>
+ * <p>
+ * The following required fields are automatically populated by the
+ * helper class and may not be overridden:
+ * <ul>
+ * <li>package name, set to the package of the host view's
+ * {@link Context}, see {@link AccessibilityEvent#setPackageName}
+ * <li>event source, set to the host view and virtual view identifier,
+ * see {@link android.view.accessibility.AccessibilityRecord#setSource(View, int)}
+ * </ul>
+ *
+ * @param virtualViewId The virtual view id for the item for which to
+ * populate the event
+ * @param event The event to populate
+ */
+ protected abstract void onPopulateEventForVirtualView(
+ int virtualViewId, AccessibilityEvent event);
+
+ /**
+ * Populates an {@link AccessibilityEvent} with information about the host
+ * view.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param event the event to populate with information about the host view
+ */
+ protected void onPopulateEventForHost(AccessibilityEvent event) {
+ // Default implementation is no-op.
+ }
+
+ /**
+ * Populates an {@link AccessibilityNodeInfo} with information
+ * about the specified item.
+ * <p>
+ * Implementations <b>must</b> populate the following required fields:
+ * <ul>
+ * <li>event text, see {@link AccessibilityNodeInfo#setText} or
+ * {@link AccessibilityNodeInfo#setContentDescription}
+ * <li>bounds in parent coordinates, see
+ * {@link AccessibilityNodeInfo#setBoundsInParent}
+ * </ul>
+ * <p>
+ * The helper class automatically populates the following fields with
+ * default values, but implementations may optionally override them:
+ * <ul>
+ * <li>enabled state, set to true, see
+ * {@link AccessibilityNodeInfo#setEnabled}
+ * <li>item class name, identical to the class name set by
+ * {@link #onPopulateEventForVirtualView}, see
+ * {@link AccessibilityNodeInfo#setClassName}
+ * </ul>
+ * <p>
+ * The following required fields are automatically populated by the
+ * helper class and may not be overridden:
+ * <ul>
+ * <li>package name, identical to the package name set by
+ * {@link #onPopulateEventForVirtualView}, see
+ * {@link AccessibilityNodeInfo#setPackageName}
+ * <li>node source, identical to the event source set in
+ * {@link #onPopulateEventForVirtualView}, see
+ * {@link AccessibilityNodeInfo#setSource(View, int)}
+ * <li>parent view, set to the host view, see
+ * {@link AccessibilityNodeInfo#setParent(View)}
+ * <li>visibility, computed based on parent-relative bounds, see
+ * {@link AccessibilityNodeInfo#setVisibleToUser}
+ * <li>accessibility focus, computed based on internal helper state, see
+ * {@link AccessibilityNodeInfo#setAccessibilityFocused}
+ * <li>bounds in screen coordinates, computed based on host view bounds,
+ * see {@link AccessibilityNodeInfo#setBoundsInScreen}
+ * </ul>
+ * <p>
+ * Additionally, the helper class automatically handles accessibility
+ * focus management by adding the appropriate
+ * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or
+ * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
+ * action. Implementations must <b>never</b> manually add these actions.
+ * <p>
+ * The helper class also automatically modifies parent- and
+ * screen-relative bounds to reflect the portion of the item visible
+ * within its parent.
+ *
+ * @param virtualViewId The virtual view identifier of the item for
+ * which to populate the node
+ * @param node The node to populate
+ */
+ protected abstract void onPopulateNodeForVirtualView(
+ int virtualViewId, AccessibilityNodeInfo node);
+
+ /**
+ * Populates an {@link AccessibilityNodeInfo} with information about the
+ * host view.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param node the node to populate with information about the host view
+ */
+ protected void onPopulateNodeForHost(AccessibilityNodeInfo node) {
+ // Default implementation is no-op.
+ }
+
+ /**
+ * Performs the specified accessibility action on the item associated
+ * with the virtual view identifier. See
+ * {@link AccessibilityNodeInfo#performAction(int, Bundle)} for
+ * more information.
+ * <p>
+ * Implementations <b>must</b> handle any actions added manually in
+ * {@link #onPopulateNodeForVirtualView}.
+ * <p>
+ * The helper class automatically handles focus management resulting
+ * from {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS}
+ * and
+ * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
+ * actions.
+ *
+ * @param virtualViewId The virtual view identifier of the item on which
+ * to perform the action
+ * @param action The accessibility action to perform
+ * @param arguments (Optional) A bundle with additional arguments, or
+ * null
+ * @return true if the action was performed
+ */
+ protected abstract boolean onPerformActionForVirtualView(
+ int virtualViewId, int action, Bundle arguments);
+
+ /**
+ * Exposes a virtual view hierarchy to the accessibility framework. Only
+ * used in API 16+.
+ */
+ private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider {
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ return ExploreByTouchHelper.this.createNode(virtualViewId);
+ }
+
+ @Override
+ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments);
+ }
+ }
+}
diff --git a/com/android/internal/widget/FloatingToolbar.java b/com/android/internal/widget/FloatingToolbar.java
new file mode 100644
index 0000000..d7611dc
--- /dev/null
+++ b/com/android/internal/widget/FloatingToolbar.java
@@ -0,0 +1,1777 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Size;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A floating toolbar for showing contextual menu items.
+ * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
+ * the remaining menu items in a vertical overflow view when the overflow button is clicked.
+ * The horizontal toolbar morphs into the vertical overflow view.
+ */
+public final class FloatingToolbar {
+
+ // This class is responsible for the public API of the floating toolbar.
+ // It delegates rendering operations to the FloatingToolbarPopup.
+
+ public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
+
+ private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
+ item -> false;
+
+ private final Context mContext;
+ private final Window mWindow;
+ private final FloatingToolbarPopup mPopup;
+
+ private final Rect mContentRect = new Rect();
+ private final Rect mPreviousContentRect = new Rect();
+
+ private Menu mMenu;
+ private List<MenuItem> mShowingMenuItems = new ArrayList<>();
+ private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
+
+ private int mSuggestedWidth;
+ private boolean mWidthChanged = true;
+
+ private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
+
+ private final Rect mNewRect = new Rect();
+ private final Rect mOldRect = new Rect();
+
+ @Override
+ public void onLayoutChange(
+ View view,
+ int newLeft, int newRight, int newTop, int newBottom,
+ int oldLeft, int oldRight, int oldTop, int oldBottom) {
+ mNewRect.set(newLeft, newRight, newTop, newBottom);
+ mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
+ if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
+ mWidthChanged = true;
+ updateLayout();
+ }
+ }
+ };
+
+ /**
+ * Sorts the list of menu items to conform to certain requirements.
+ */
+ private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> {
+ // Ensure the assist menu item is always the first item:
+ if (menuItem1.getItemId() == android.R.id.textAssist) {
+ return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1;
+ }
+ if (menuItem2.getItemId() == android.R.id.textAssist) {
+ return 1;
+ }
+
+ // Order by SHOW_AS_ACTION type:
+ if (menuItem1.requiresActionButton()) {
+ return menuItem2.requiresActionButton() ? 0 : -1;
+ }
+ if (menuItem2.requiresActionButton()) {
+ return 1;
+ }
+ if (menuItem1.requiresOverflow()) {
+ return menuItem2.requiresOverflow() ? 0 : 1;
+ }
+ if (menuItem2.requiresOverflow()) {
+ return -1;
+ }
+
+ // Order by order value:
+ return menuItem1.getOrder() - menuItem2.getOrder();
+ };
+
+ /**
+ * Initializes a floating toolbar.
+ */
+ public FloatingToolbar(Window window) {
+ // TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
+ // supports multi-display.
+ mContext = applyDefaultTheme(window.getContext());
+ mWindow = Objects.requireNonNull(window);
+ mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
+ }
+
+ /**
+ * Sets the menu to be shown in this floating toolbar.
+ * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
+ * toolbar.
+ */
+ public FloatingToolbar setMenu(Menu menu) {
+ mMenu = Objects.requireNonNull(menu);
+ return this;
+ }
+
+ /**
+ * Sets the custom listener for invocation of menu items in this floating toolbar.
+ */
+ public FloatingToolbar setOnMenuItemClickListener(
+ MenuItem.OnMenuItemClickListener menuItemClickListener) {
+ if (menuItemClickListener != null) {
+ mMenuItemClickListener = menuItemClickListener;
+ } else {
+ mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the content rectangle. This is the area of the interesting content that this toolbar
+ * should avoid obstructing.
+ * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
+ * toolbar.
+ */
+ public FloatingToolbar setContentRect(Rect rect) {
+ mContentRect.set(Objects.requireNonNull(rect));
+ return this;
+ }
+
+ /**
+ * Sets the suggested width of this floating toolbar.
+ * The actual width will be about this size but there are no guarantees that it will be exactly
+ * the suggested width.
+ * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
+ * toolbar.
+ */
+ public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
+ // Check if there's been a substantial width spec change.
+ int difference = Math.abs(suggestedWidth - mSuggestedWidth);
+ mWidthChanged = difference > (mSuggestedWidth * 0.2);
+
+ mSuggestedWidth = suggestedWidth;
+ return this;
+ }
+
+ /**
+ * Shows this floating toolbar.
+ */
+ public FloatingToolbar show() {
+ registerOrientationHandler();
+ doShow();
+ return this;
+ }
+
+ /**
+ * Updates this floating toolbar to reflect recent position and view updates.
+ * NOTE: This method is a no-op if the toolbar isn't showing.
+ */
+ public FloatingToolbar updateLayout() {
+ if (mPopup.isShowing()) {
+ doShow();
+ }
+ return this;
+ }
+
+ /**
+ * Dismisses this floating toolbar.
+ */
+ public void dismiss() {
+ unregisterOrientationHandler();
+ mPopup.dismiss();
+ }
+
+ /**
+ * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
+ * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
+ */
+ public void hide() {
+ mPopup.hide();
+ }
+
+ /**
+ * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
+ */
+ public boolean isShowing() {
+ return mPopup.isShowing();
+ }
+
+ /**
+ * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
+ */
+ public boolean isHidden() {
+ return mPopup.isHidden();
+ }
+
+ /**
+ * If this is set to true, the action mode view will dismiss itself on touch events outside of
+ * its window. If the toolbar is already showing, it will be re-shown so that this setting takes
+ * effect immediately.
+ *
+ * @param outsideTouchable whether or not this action mode is "outside touchable"
+ * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
+ */
+ public void setOutsideTouchable(
+ boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
+ if (mPopup.setOutsideTouchable(outsideTouchable, onDismiss) && isShowing()) {
+ dismiss();
+ doShow();
+ }
+ }
+
+ private void doShow() {
+ List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
+ menuItems.sort(mMenuItemComparator);
+ if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
+ mPopup.dismiss();
+ mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
+ mShowingMenuItems = menuItems;
+ }
+ if (!mPopup.isShowing()) {
+ mPopup.show(mContentRect);
+ } else if (!mPreviousContentRect.equals(mContentRect)) {
+ mPopup.updateCoordinates(mContentRect);
+ }
+ mWidthChanged = false;
+ mPreviousContentRect.set(mContentRect);
+ }
+
+ /**
+ * Returns true if this floating toolbar is currently showing the specified menu items.
+ */
+ private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
+ if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) {
+ return false;
+ }
+
+ final int size = menuItems.size();
+ for (int i = 0; i < size; i++) {
+ final MenuItem menuItem = menuItems.get(i);
+ final MenuItem showingItem = mShowingMenuItems.get(i);
+ if (menuItem.getItemId() != showingItem.getItemId()
+ || !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle())
+ || !Objects.equals(menuItem.getIcon(), showingItem.getIcon())
+ || menuItem.getGroupId() != showingItem.getGroupId()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the visible and enabled menu items in the specified menu.
+ * This method is recursive.
+ */
+ private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
+ List<MenuItem> menuItems = new ArrayList<>();
+ for (int i = 0; (menu != null) && (i < menu.size()); i++) {
+ MenuItem menuItem = menu.getItem(i);
+ if (menuItem.isVisible() && menuItem.isEnabled()) {
+ Menu subMenu = menuItem.getSubMenu();
+ if (subMenu != null) {
+ menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
+ } else {
+ menuItems.add(menuItem);
+ }
+ }
+ }
+ return menuItems;
+ }
+
+ private void registerOrientationHandler() {
+ unregisterOrientationHandler();
+ mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
+ }
+
+ private void unregisterOrientationHandler() {
+ mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
+ }
+
+
+ /**
+ * A popup window used by the floating toolbar.
+ *
+ * This class is responsible for the rendering/animation of the floating toolbar.
+ * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
+ * to transition between panels.
+ */
+ private static final class FloatingToolbarPopup {
+
+ /* Minimum and maximum number of items allowed in the overflow. */
+ private static final int MIN_OVERFLOW_SIZE = 2;
+ private static final int MAX_OVERFLOW_SIZE = 4;
+
+ private final Context mContext;
+ private final View mParent; // Parent for the popup window.
+ private final PopupWindow mPopupWindow;
+
+ /* Margins between the popup window and it's content. */
+ private final int mMarginHorizontal;
+ private final int mMarginVertical;
+
+ /* View components */
+ private final ViewGroup mContentContainer; // holds all contents.
+ private final ViewGroup mMainPanel; // holds menu items that are initially displayed.
+ private final OverflowPanel mOverflowPanel; // holds menu items hidden in the overflow.
+ private final ImageButton mOverflowButton; // opens/closes the overflow.
+ /* overflow button drawables. */
+ private final Drawable mArrow;
+ private final Drawable mOverflow;
+ private final AnimatedVectorDrawable mToArrow;
+ private final AnimatedVectorDrawable mToOverflow;
+
+ private final OverflowPanelViewHelper mOverflowPanelViewHelper;
+
+ /* Animation interpolators. */
+ private final Interpolator mLogAccelerateInterpolator;
+ private final Interpolator mFastOutSlowInInterpolator;
+ private final Interpolator mLinearOutSlowInInterpolator;
+ private final Interpolator mFastOutLinearInInterpolator;
+
+ /* Animations. */
+ private final AnimatorSet mShowAnimation;
+ private final AnimatorSet mDismissAnimation;
+ private final AnimatorSet mHideAnimation;
+ private final AnimationSet mOpenOverflowAnimation;
+ private final AnimationSet mCloseOverflowAnimation;
+ private final Animation.AnimationListener mOverflowAnimationListener;
+
+ private final Rect mViewPortOnScreen = new Rect(); // portion of screen we can draw in.
+ private final Point mCoordsOnWindow = new Point(); // popup window coordinates.
+ /* Temporary data holders. Reset values before using. */
+ private final int[] mTmpCoords = new int[2];
+
+ private final Region mTouchableRegion = new Region();
+ private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
+ info -> {
+ info.contentInsets.setEmpty();
+ info.visibleInsets.setEmpty();
+ info.touchableRegion.set(mTouchableRegion);
+ info.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ };
+
+ private final int mLineHeight;
+ private final int mIconTextSpacing;
+
+ /**
+ * @see OverflowPanelViewHelper#preparePopupContent().
+ */
+ private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
+ @Override
+ public void run() {
+ setPanelsStatesAtRestingPosition();
+ setContentAreaAsTouchableSurface();
+ mContentContainer.setAlpha(1);
+ }
+ };
+
+ private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
+ private boolean mHidden; // tracks whether this popup is hidden or hiding.
+
+ /* Calculated sizes for panels and overflow button. */
+ private final Size mOverflowButtonSize;
+ private Size mOverflowPanelSize; // Should be null when there is no overflow.
+ private Size mMainPanelSize;
+
+ /* Item click listeners */
+ private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
+ private final View.OnClickListener mMenuItemButtonOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (v.getTag() instanceof MenuItem) {
+ if (mOnMenuItemClickListener != null) {
+ mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
+ }
+ }
+ }
+ };
+
+ private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards.
+ private boolean mIsOverflowOpen;
+
+ private int mTransitionDurationScale; // Used to scale the toolbar transition duration.
+
+ /**
+ * Initializes a new floating toolbar popup.
+ *
+ * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
+ * from.
+ */
+ public FloatingToolbarPopup(Context context, View parent) {
+ mParent = Objects.requireNonNull(parent);
+ mContext = Objects.requireNonNull(context);
+ mContentContainer = createContentContainer(context);
+ mPopupWindow = createPopupWindow(mContentContainer);
+ mMarginHorizontal = parent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
+ mMarginVertical = parent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
+ mLineHeight = context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_height);
+ mIconTextSpacing = context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
+
+ // Interpolators
+ mLogAccelerateInterpolator = new LogAccelerateInterpolator();
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_slow_in);
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.linear_out_slow_in);
+ mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_linear_in);
+
+ // Drawables. Needed for views.
+ mArrow = mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
+ mArrow.setAutoMirrored(true);
+ mOverflow = mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
+ mOverflow.setAutoMirrored(true);
+ mToArrow = (AnimatedVectorDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
+ mToArrow.setAutoMirrored(true);
+ mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
+ .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
+ mToOverflow.setAutoMirrored(true);
+
+ // Views
+ mOverflowButton = createOverflowButton();
+ mOverflowButtonSize = measure(mOverflowButton);
+ mMainPanel = createMainPanel();
+ mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext, mIconTextSpacing);
+ mOverflowPanel = createOverflowPanel();
+
+ // Animation. Need views.
+ mOverflowAnimationListener = createOverflowAnimationListener();
+ mOpenOverflowAnimation = new AnimationSet(true);
+ mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
+ mCloseOverflowAnimation = new AnimationSet(true);
+ mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
+ mShowAnimation = createEnterAnimation(mContentContainer);
+ mDismissAnimation = createExitAnimation(
+ mContentContainer,
+ 150, // startDelay
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPopupWindow.dismiss();
+ mContentContainer.removeAllViews();
+ }
+ });
+ mHideAnimation = createExitAnimation(
+ mContentContainer,
+ 0, // startDelay
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPopupWindow.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Makes this toolbar "outside touchable" and sets the onDismissListener.
+ * This will take effect the next time the toolbar is re-shown.
+ *
+ * @param outsideTouchable if true, the popup will be made "outside touchable" and
+ * "non focusable". The reverse will happen if false.
+ * @param onDismiss
+ *
+ * @return true if the "outsideTouchable" setting was modified. Otherwise returns false
+ *
+ * @see PopupWindow#setOutsideTouchable(boolean)
+ * @see PopupWindow#setFocusable(boolean)
+ * @see PopupWindow.OnDismissListener
+ */
+ public boolean setOutsideTouchable(
+ boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
+ boolean ret = false;
+ if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) {
+ mPopupWindow.setOutsideTouchable(outsideTouchable);
+ mPopupWindow.setFocusable(!outsideTouchable);
+ ret = true;
+ }
+ mPopupWindow.setOnDismissListener(onDismiss);
+ return ret;
+ }
+
+ /**
+ * Lays out buttons for the specified menu items.
+ * Requires a subsequent call to {@link #show()} to show the items.
+ */
+ public void layoutMenuItems(
+ List<MenuItem> menuItems,
+ MenuItem.OnMenuItemClickListener menuItemClickListener,
+ int suggestedWidth) {
+ mOnMenuItemClickListener = menuItemClickListener;
+ cancelOverflowAnimations();
+ clearPanels();
+ menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
+ if (!menuItems.isEmpty()) {
+ // Add remaining items to the overflow.
+ layoutOverflowPanelItems(menuItems);
+ }
+ updatePopupSize();
+ }
+
+ /**
+ * Shows this popup at the specified coordinates.
+ * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
+ */
+ public void show(Rect contentRectOnScreen) {
+ Objects.requireNonNull(contentRectOnScreen);
+
+ if (isShowing()) {
+ return;
+ }
+
+ mHidden = false;
+ mDismissed = false;
+ cancelDismissAndHideAnimations();
+ cancelOverflowAnimations();
+
+ refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
+ preparePopupContent();
+ // We need to specify the position in window coordinates.
+ // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
+ // specify the popup position in screen coordinates.
+ mPopupWindow.showAtLocation(
+ mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
+ setTouchableSurfaceInsetsComputer();
+ runShowAnimation();
+ }
+
+ /**
+ * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
+ */
+ public void dismiss() {
+ if (mDismissed) {
+ return;
+ }
+
+ mHidden = false;
+ mDismissed = true;
+ mHideAnimation.cancel();
+
+ runDismissAnimation();
+ setZeroTouchableSurface();
+ }
+
+ /**
+ * Hides this popup. This is a no-op if this popup is not showing.
+ * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
+ */
+ public void hide() {
+ if (!isShowing()) {
+ return;
+ }
+
+ mHidden = true;
+ runHideAnimation();
+ setZeroTouchableSurface();
+ }
+
+ /**
+ * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
+ */
+ public boolean isShowing() {
+ return !mDismissed && !mHidden;
+ }
+
+ /**
+ * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
+ */
+ public boolean isHidden() {
+ return mHidden;
+ }
+
+ /**
+ * Updates the coordinates of this popup.
+ * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
+ * This is a no-op if this popup is not showing.
+ */
+ public void updateCoordinates(Rect contentRectOnScreen) {
+ Objects.requireNonNull(contentRectOnScreen);
+
+ if (!isShowing() || !mPopupWindow.isShowing()) {
+ return;
+ }
+
+ cancelOverflowAnimations();
+ refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
+ preparePopupContent();
+ // We need to specify the position in window coordinates.
+ // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
+ // specify the popup position in screen coordinates.
+ mPopupWindow.update(
+ mCoordsOnWindow.x, mCoordsOnWindow.y,
+ mPopupWindow.getWidth(), mPopupWindow.getHeight());
+ }
+
+ private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
+ refreshViewPort();
+
+ // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
+ // landscape.
+ final int x = Math.min(
+ contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
+ mViewPortOnScreen.right - mPopupWindow.getWidth());
+
+ final int y;
+
+ final int availableHeightAboveContent =
+ contentRectOnScreen.top - mViewPortOnScreen.top;
+ final int availableHeightBelowContent =
+ mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
+
+ final int margin = 2 * mMarginVertical;
+ final int toolbarHeightWithVerticalMargin = mLineHeight + margin;
+
+ if (!hasOverflow()) {
+ if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
+ // There is enough space at the top of the content.
+ y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
+ } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
+ // There is enough space at the bottom of the content.
+ y = contentRectOnScreen.bottom;
+ } else if (availableHeightBelowContent >= mLineHeight) {
+ // Just enough space to fit the toolbar with no vertical margins.
+ y = contentRectOnScreen.bottom - mMarginVertical;
+ } else {
+ // Not enough space. Prefer to position as high as possible.
+ y = Math.max(
+ mViewPortOnScreen.top,
+ contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
+ }
+ } else {
+ // Has an overflow.
+ final int minimumOverflowHeightWithMargin =
+ calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
+ final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
+ contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
+ final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
+ mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
+
+ if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the top of the content rect for the overflow.
+ // Position above and open upwards.
+ updateOverflowHeight(availableHeightAboveContent - margin);
+ y = contentRectOnScreen.top - mPopupWindow.getHeight();
+ mOpenOverflowUpwards = true;
+ } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
+ && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the top of the content rect for the main panel
+ // but not the overflow.
+ // Position above but open downwards.
+ updateOverflowHeight(availableHeightThroughContentDown - margin);
+ y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
+ mOpenOverflowUpwards = false;
+ } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the bottom of the content rect for the overflow.
+ // Position below and open downwards.
+ updateOverflowHeight(availableHeightBelowContent - margin);
+ y = contentRectOnScreen.bottom;
+ mOpenOverflowUpwards = false;
+ } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
+ && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
+ // There is enough space at the bottom of the content rect for the main panel
+ // but not the overflow.
+ // Position below but open upwards.
+ updateOverflowHeight(availableHeightThroughContentUp - margin);
+ y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
+ mPopupWindow.getHeight();
+ mOpenOverflowUpwards = true;
+ } else {
+ // Not enough space.
+ // Position at the top of the view port and open downwards.
+ updateOverflowHeight(mViewPortOnScreen.height() - margin);
+ y = mViewPortOnScreen.top;
+ mOpenOverflowUpwards = false;
+ }
+ }
+
+ // We later specify the location of PopupWindow relative to the attached window.
+ // The idea here is that 1) we can get the location of a View in both window coordinates
+ // and screen coordiantes, where the offset between them should be equal to the window
+ // origin, and 2) we can use an arbitrary for this calculation while calculating the
+ // location of the rootview is supposed to be least expensive.
+ // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can avoid
+ // the following calculation.
+ mParent.getRootView().getLocationOnScreen(mTmpCoords);
+ int rootViewLeftOnScreen = mTmpCoords[0];
+ int rootViewTopOnScreen = mTmpCoords[1];
+ mParent.getRootView().getLocationInWindow(mTmpCoords);
+ int rootViewLeftOnWindow = mTmpCoords[0];
+ int rootViewTopOnWindow = mTmpCoords[1];
+ int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
+ int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
+ mCoordsOnWindow.set(
+ Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
+ }
+
+ /**
+ * Performs the "show" animation on the floating popup.
+ */
+ private void runShowAnimation() {
+ mShowAnimation.start();
+ }
+
+ /**
+ * Performs the "dismiss" animation on the floating popup.
+ */
+ private void runDismissAnimation() {
+ mDismissAnimation.start();
+ }
+
+ /**
+ * Performs the "hide" animation on the floating popup.
+ */
+ private void runHideAnimation() {
+ mHideAnimation.start();
+ }
+
+ private void cancelDismissAndHideAnimations() {
+ mDismissAnimation.cancel();
+ mHideAnimation.cancel();
+ }
+
+ private void cancelOverflowAnimations() {
+ mContentContainer.clearAnimation();
+ mMainPanel.animate().cancel();
+ mOverflowPanel.animate().cancel();
+ mToArrow.stop();
+ mToOverflow.stop();
+ }
+
+ private void openOverflow() {
+ final int targetWidth = mOverflowPanelSize.getWidth();
+ final int targetHeight = mOverflowPanelSize.getHeight();
+ final int startWidth = mContentContainer.getWidth();
+ final int startHeight = mContentContainer.getHeight();
+ final float startY = mContentContainer.getY();
+ final float left = mContentContainer.getX();
+ final float right = left + mContentContainer.getWidth();
+ Animation widthAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+ setWidth(mContentContainer, startWidth + deltaWidth);
+ if (isInRTLMode()) {
+ mContentContainer.setX(left);
+
+ // Lock the panels in place.
+ mMainPanel.setX(0);
+ mOverflowPanel.setX(0);
+ } else {
+ mContentContainer.setX(right - mContentContainer.getWidth());
+
+ // Offset the panels' positions so they look like they're locked in place
+ // on the screen.
+ mMainPanel.setX(mContentContainer.getWidth() - startWidth);
+ mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
+ }
+ }
+ };
+ Animation heightAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+ setHeight(mContentContainer, startHeight + deltaHeight);
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY(
+ startY - (mContentContainer.getHeight() - startHeight));
+ positionContentYCoordinatesIfOpeningOverflowUpwards();
+ }
+ }
+ };
+ final float overflowButtonStartX = mOverflowButton.getX();
+ final float overflowButtonTargetX = isInRTLMode() ?
+ overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
+ overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
+ Animation overflowButtonAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float overflowButtonX = overflowButtonStartX
+ + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
+ float deltaContainerWidth = isInRTLMode() ?
+ 0 :
+ mContentContainer.getWidth() - startWidth;
+ float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
+ mOverflowButton.setX(actualOverflowButtonX);
+ }
+ };
+ widthAnimation.setInterpolator(mLogAccelerateInterpolator);
+ widthAnimation.setDuration(getAdjustedDuration(250));
+ heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ heightAnimation.setDuration(getAdjustedDuration(250));
+ overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ overflowButtonAnimation.setDuration(getAdjustedDuration(250));
+ mOpenOverflowAnimation.getAnimations().clear();
+ mOpenOverflowAnimation.getAnimations().clear();
+ mOpenOverflowAnimation.addAnimation(widthAnimation);
+ mOpenOverflowAnimation.addAnimation(heightAnimation);
+ mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
+ mContentContainer.startAnimation(mOpenOverflowAnimation);
+ mIsOverflowOpen = true;
+ mMainPanel.animate()
+ .alpha(0).withLayer()
+ .setInterpolator(mLinearOutSlowInInterpolator)
+ .setDuration(250)
+ .start();
+ mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
+ }
+
+ private void closeOverflow() {
+ final int targetWidth = mMainPanelSize.getWidth();
+ final int startWidth = mContentContainer.getWidth();
+ final float left = mContentContainer.getX();
+ final float right = left + mContentContainer.getWidth();
+ Animation widthAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
+ setWidth(mContentContainer, startWidth + deltaWidth);
+ if (isInRTLMode()) {
+ mContentContainer.setX(left);
+
+ // Lock the panels in place.
+ mMainPanel.setX(0);
+ mOverflowPanel.setX(0);
+ } else {
+ mContentContainer.setX(right - mContentContainer.getWidth());
+
+ // Offset the panels' positions so they look like they're locked in place
+ // on the screen.
+ mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
+ mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
+ }
+ }
+ };
+ final int targetHeight = mMainPanelSize.getHeight();
+ final int startHeight = mContentContainer.getHeight();
+ final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
+ Animation heightAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
+ setHeight(mContentContainer, startHeight + deltaHeight);
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY(bottom - mContentContainer.getHeight());
+ positionContentYCoordinatesIfOpeningOverflowUpwards();
+ }
+ }
+ };
+ final float overflowButtonStartX = mOverflowButton.getX();
+ final float overflowButtonTargetX = isInRTLMode() ?
+ overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
+ overflowButtonStartX + startWidth - mOverflowButton.getWidth();
+ Animation overflowButtonAnimation = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float overflowButtonX = overflowButtonStartX
+ + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
+ float deltaContainerWidth = isInRTLMode() ?
+ 0 :
+ mContentContainer.getWidth() - startWidth;
+ float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
+ mOverflowButton.setX(actualOverflowButtonX);
+ }
+ };
+ widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ widthAnimation.setDuration(getAdjustedDuration(250));
+ heightAnimation.setInterpolator(mLogAccelerateInterpolator);
+ heightAnimation.setDuration(getAdjustedDuration(250));
+ overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
+ overflowButtonAnimation.setDuration(getAdjustedDuration(250));
+ mCloseOverflowAnimation.getAnimations().clear();
+ mCloseOverflowAnimation.addAnimation(widthAnimation);
+ mCloseOverflowAnimation.addAnimation(heightAnimation);
+ mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
+ mContentContainer.startAnimation(mCloseOverflowAnimation);
+ mIsOverflowOpen = false;
+ mMainPanel.animate()
+ .alpha(1).withLayer()
+ .setInterpolator(mFastOutLinearInInterpolator)
+ .setDuration(100)
+ .start();
+ mOverflowPanel.animate()
+ .alpha(0).withLayer()
+ .setInterpolator(mLinearOutSlowInInterpolator)
+ .setDuration(150)
+ .start();
+ }
+
+ /**
+ * Defines the position of the floating toolbar popup panels when transition animation has
+ * stopped.
+ */
+ private void setPanelsStatesAtRestingPosition() {
+ mOverflowButton.setEnabled(true);
+ mOverflowPanel.awakenScrollBars();
+
+ if (mIsOverflowOpen) {
+ // Set open state.
+ final Size containerSize = mOverflowPanelSize;
+ setSize(mContentContainer, containerSize);
+ mMainPanel.setAlpha(0);
+ mMainPanel.setVisibility(View.INVISIBLE);
+ mOverflowPanel.setAlpha(1);
+ mOverflowPanel.setVisibility(View.VISIBLE);
+ mOverflowButton.setImageDrawable(mArrow);
+ mOverflowButton.setContentDescription(mContext.getString(
+ R.string.floating_toolbar_close_overflow_description));
+
+ // Update x-coordinates depending on RTL state.
+ if (isInRTLMode()) {
+ mContentContainer.setX(mMarginHorizontal); // align left
+ mMainPanel.setX(0); // align left
+ mOverflowButton.setX( // align right
+ containerSize.getWidth() - mOverflowButtonSize.getWidth());
+ mOverflowPanel.setX(0); // align left
+ } else {
+ mContentContainer.setX( // align right
+ mPopupWindow.getWidth() -
+ containerSize.getWidth() - mMarginHorizontal);
+ mMainPanel.setX(-mContentContainer.getX()); // align right
+ mOverflowButton.setX(0); // align left
+ mOverflowPanel.setX(0); // align left
+ }
+
+ // Update y-coordinates depending on overflow's open direction.
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setY( // align bottom
+ containerSize.getHeight() - mContentContainer.getHeight());
+ mOverflowButton.setY( // align bottom
+ containerSize.getHeight() - mOverflowButtonSize.getHeight());
+ mOverflowPanel.setY(0); // align top
+ } else {
+ // opens downwards.
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setY(0); // align top
+ mOverflowButton.setY(0); // align top
+ mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
+ }
+ } else {
+ // Overflow not open. Set closed state.
+ final Size containerSize = mMainPanelSize;
+ setSize(mContentContainer, containerSize);
+ mMainPanel.setAlpha(1);
+ mMainPanel.setVisibility(View.VISIBLE);
+ mOverflowPanel.setAlpha(0);
+ mOverflowPanel.setVisibility(View.INVISIBLE);
+ mOverflowButton.setImageDrawable(mOverflow);
+ mOverflowButton.setContentDescription(mContext.getString(
+ R.string.floating_toolbar_open_overflow_description));
+
+ if (hasOverflow()) {
+ // Update x-coordinates depending on RTL state.
+ if (isInRTLMode()) {
+ mContentContainer.setX(mMarginHorizontal); // align left
+ mMainPanel.setX(0); // align left
+ mOverflowButton.setX(0); // align left
+ mOverflowPanel.setX(0); // align left
+ } else {
+ mContentContainer.setX( // align right
+ mPopupWindow.getWidth() -
+ containerSize.getWidth() - mMarginHorizontal);
+ mMainPanel.setX(0); // align left
+ mOverflowButton.setX( // align right
+ containerSize.getWidth() - mOverflowButtonSize.getWidth());
+ mOverflowPanel.setX( // align right
+ containerSize.getWidth() - mOverflowPanelSize.getWidth());
+ }
+
+ // Update y-coordinates depending on overflow's open direction.
+ if (mOpenOverflowUpwards) {
+ mContentContainer.setY( // align bottom
+ mMarginVertical +
+ mOverflowPanelSize.getHeight() - containerSize.getHeight());
+ mMainPanel.setY(0); // align top
+ mOverflowButton.setY(0); // align top
+ mOverflowPanel.setY( // align bottom
+ containerSize.getHeight() - mOverflowPanelSize.getHeight());
+ } else {
+ // opens downwards.
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setY(0); // align top
+ mOverflowButton.setY(0); // align top
+ mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
+ }
+ } else {
+ // No overflow.
+ mContentContainer.setX(mMarginHorizontal); // align left
+ mContentContainer.setY(mMarginVertical); // align top
+ mMainPanel.setX(0); // align left
+ mMainPanel.setY(0); // align top
+ }
+ }
+ }
+
+ private void updateOverflowHeight(int suggestedHeight) {
+ if (hasOverflow()) {
+ final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
+ mLineHeight;
+ final int newHeight = calculateOverflowHeight(maxItemSize);
+ if (mOverflowPanelSize.getHeight() != newHeight) {
+ mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
+ }
+ setSize(mOverflowPanel, mOverflowPanelSize);
+ if (mIsOverflowOpen) {
+ setSize(mContentContainer, mOverflowPanelSize);
+ if (mOpenOverflowUpwards) {
+ final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
+ mContentContainer.setY(mContentContainer.getY() + deltaHeight);
+ mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
+ }
+ } else {
+ setSize(mContentContainer, mMainPanelSize);
+ }
+ updatePopupSize();
+ }
+ }
+
+ private void updatePopupSize() {
+ int width = 0;
+ int height = 0;
+ if (mMainPanelSize != null) {
+ width = Math.max(width, mMainPanelSize.getWidth());
+ height = Math.max(height, mMainPanelSize.getHeight());
+ }
+ if (mOverflowPanelSize != null) {
+ width = Math.max(width, mOverflowPanelSize.getWidth());
+ height = Math.max(height, mOverflowPanelSize.getHeight());
+ }
+ mPopupWindow.setWidth(width + mMarginHorizontal * 2);
+ mPopupWindow.setHeight(height + mMarginVertical * 2);
+ maybeComputeTransitionDurationScale();
+ }
+
+ private void refreshViewPort() {
+ mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
+ }
+
+ private int getAdjustedToolbarWidth(int suggestedWidth) {
+ int width = suggestedWidth;
+ refreshViewPort();
+ int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
+ if (width <= 0) {
+ width = mParent.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
+ }
+ return Math.min(width, maximumWidth);
+ }
+
+ /**
+ * Sets the touchable region of this popup to be zero. This means that all touch events on
+ * this popup will go through to the surface behind it.
+ */
+ private void setZeroTouchableSurface() {
+ mTouchableRegion.setEmpty();
+ }
+
+ /**
+ * Sets the touchable region of this popup to be the area occupied by its content.
+ */
+ private void setContentAreaAsTouchableSurface() {
+ Objects.requireNonNull(mMainPanelSize);
+ final int width;
+ final int height;
+ if (mIsOverflowOpen) {
+ Objects.requireNonNull(mOverflowPanelSize);
+ width = mOverflowPanelSize.getWidth();
+ height = mOverflowPanelSize.getHeight();
+ } else {
+ width = mMainPanelSize.getWidth();
+ height = mMainPanelSize.getHeight();
+ }
+ mTouchableRegion.set(
+ (int) mContentContainer.getX(),
+ (int) mContentContainer.getY(),
+ (int) mContentContainer.getX() + width,
+ (int) mContentContainer.getY() + height);
+ }
+
+ /**
+ * Make the touchable area of this popup be the area specified by mTouchableRegion.
+ * This should be called after the popup window has been dismissed (dismiss/hide)
+ * and is probably being re-shown with a new content root view.
+ */
+ private void setTouchableSurfaceInsetsComputer() {
+ ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
+ .getRootView()
+ .getViewTreeObserver();
+ viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
+ viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
+ }
+
+ private boolean isInRTLMode() {
+ return mContext.getApplicationInfo().hasRtlSupport()
+ && mContext.getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ private boolean hasOverflow() {
+ return mOverflowPanelSize != null;
+ }
+
+ /**
+ * Fits as many menu items in the main panel and returns a list of the menu items that
+ * were not fit in.
+ *
+ * @return The menu items that are not included in this main panel.
+ */
+ public List<MenuItem> layoutMainPanelItems(
+ List<MenuItem> menuItems, final int toolbarWidth) {
+ Objects.requireNonNull(menuItems);
+
+ int availableWidth = toolbarWidth;
+
+ final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>();
+ // add the overflow menu items to the end of the remainingMenuItems list.
+ final LinkedList<MenuItem> overflowMenuItems = new LinkedList();
+ for (MenuItem menuItem : menuItems) {
+ if (menuItem.getItemId() != android.R.id.textAssist
+ && menuItem.requiresOverflow()) {
+ overflowMenuItems.add(menuItem);
+ } else {
+ remainingMenuItems.add(menuItem);
+ }
+ }
+ remainingMenuItems.addAll(overflowMenuItems);
+
+ mMainPanel.removeAllViews();
+ mMainPanel.setPaddingRelative(0, 0, 0, 0);
+
+ int lastGroupId = -1;
+ boolean isFirstItem = true;
+ while (!remainingMenuItems.isEmpty()) {
+ final MenuItem menuItem = remainingMenuItems.peek();
+
+ // if this is the first item, regardless of requiresOverflow(), it should be
+ // displayed on the main panel. Otherwise all items including this one will be
+ // overflow items, and should be displayed in overflow panel.
+ if(!isFirstItem && menuItem.requiresOverflow()) {
+ break;
+ }
+
+ final boolean showIcon = isFirstItem && menuItem.getItemId() == R.id.textAssist;
+ final View menuItemButton = createMenuItemButton(
+ mContext, menuItem, mIconTextSpacing, showIcon);
+ if (!showIcon && menuItemButton instanceof LinearLayout) {
+ ((LinearLayout) menuItemButton).setGravity(Gravity.CENTER);
+ }
+
+ // Adding additional start padding for the first button to even out button spacing.
+ if (isFirstItem) {
+ menuItemButton.setPaddingRelative(
+ (int) (1.5 * menuItemButton.getPaddingStart()),
+ menuItemButton.getPaddingTop(),
+ menuItemButton.getPaddingEnd(),
+ menuItemButton.getPaddingBottom());
+ }
+
+ // Adding additional end padding for the last button to even out button spacing.
+ boolean isLastItem = remainingMenuItems.size() == 1;
+ if (isLastItem) {
+ menuItemButton.setPaddingRelative(
+ menuItemButton.getPaddingStart(),
+ menuItemButton.getPaddingTop(),
+ (int) (1.5 * menuItemButton.getPaddingEnd()),
+ menuItemButton.getPaddingBottom());
+ }
+
+ menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ final int menuItemButtonWidth = Math.min(
+ menuItemButton.getMeasuredWidth(), toolbarWidth);
+
+ // Check if we can fit an item while reserving space for the overflowButton.
+ final boolean canFitWithOverflow =
+ menuItemButtonWidth <=
+ availableWidth - mOverflowButtonSize.getWidth();
+ final boolean canFitNoOverflow =
+ isLastItem && menuItemButtonWidth <= availableWidth;
+ if (canFitWithOverflow || canFitNoOverflow) {
+ setButtonTagAndClickListener(menuItemButton, menuItem);
+ // Set tooltips for main panel items, but not overflow items (b/35726766).
+ menuItemButton.setTooltipText(menuItem.getTooltipText());
+ mMainPanel.addView(menuItemButton);
+ final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
+ params.width = menuItemButtonWidth;
+ menuItemButton.setLayoutParams(params);
+ availableWidth -= menuItemButtonWidth;
+ remainingMenuItems.pop();
+ } else {
+ break;
+ }
+ lastGroupId = menuItem.getGroupId();
+ isFirstItem = false;
+ }
+
+ if (!remainingMenuItems.isEmpty()) {
+ // Reserve space for overflowButton.
+ mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
+ }
+
+ mMainPanelSize = measure(mMainPanel);
+ return remainingMenuItems;
+ }
+
+ private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
+ ArrayAdapter<MenuItem> overflowPanelAdapter =
+ (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
+ overflowPanelAdapter.clear();
+ final int size = menuItems.size();
+ for (int i = 0; i < size; i++) {
+ overflowPanelAdapter.add(menuItems.get(i));
+ }
+ mOverflowPanel.setAdapter(overflowPanelAdapter);
+ if (mOpenOverflowUpwards) {
+ mOverflowPanel.setY(0);
+ } else {
+ mOverflowPanel.setY(mOverflowButtonSize.getHeight());
+ }
+
+ int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
+ int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
+ mOverflowPanelSize = new Size(width, height);
+ setSize(mOverflowPanel, mOverflowPanelSize);
+ }
+
+ /**
+ * Resets the content container and appropriately position it's panels.
+ */
+ private void preparePopupContent() {
+ mContentContainer.removeAllViews();
+
+ // Add views in the specified order so they stack up as expected.
+ // Order: overflowPanel, mainPanel, overflowButton.
+ if (hasOverflow()) {
+ mContentContainer.addView(mOverflowPanel);
+ }
+ mContentContainer.addView(mMainPanel);
+ if (hasOverflow()) {
+ mContentContainer.addView(mOverflowButton);
+ }
+ setPanelsStatesAtRestingPosition();
+ setContentAreaAsTouchableSurface();
+
+ // The positioning of contents in RTL is wrong when the view is first rendered.
+ // Hide the view and post a runnable to recalculate positions and render the view.
+ // TODO: Investigate why this happens and fix.
+ if (isInRTLMode()) {
+ mContentContainer.setAlpha(0);
+ mContentContainer.post(mPreparePopupContentRTLHelper);
+ }
+ }
+
+ /**
+ * Clears out the panels and their container. Resets their calculated sizes.
+ */
+ private void clearPanels() {
+ mOverflowPanelSize = null;
+ mMainPanelSize = null;
+ mIsOverflowOpen = false;
+ mMainPanel.removeAllViews();
+ ArrayAdapter<MenuItem> overflowPanelAdapter =
+ (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
+ overflowPanelAdapter.clear();
+ mOverflowPanel.setAdapter(overflowPanelAdapter);
+ mContentContainer.removeAllViews();
+ }
+
+ private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
+ if (mOpenOverflowUpwards) {
+ mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
+ mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
+ mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
+ }
+ }
+
+ private int getOverflowWidth() {
+ int overflowWidth = 0;
+ final int count = mOverflowPanel.getAdapter().getCount();
+ for (int i = 0; i < count; i++) {
+ MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
+ overflowWidth =
+ Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
+ }
+ return overflowWidth;
+ }
+
+ private int calculateOverflowHeight(int maxItemSize) {
+ // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
+ int actualSize = Math.min(
+ MAX_OVERFLOW_SIZE,
+ Math.min(
+ Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
+ mOverflowPanel.getCount()));
+ int extension = 0;
+ if (actualSize < mOverflowPanel.getCount()) {
+ // The overflow will require scrolling to get to all the items.
+ // Extend the height so that part of the hidden items is displayed.
+ extension = (int) (mLineHeight * 0.5f);
+ }
+ return actualSize * mLineHeight
+ + mOverflowButtonSize.getHeight()
+ + extension;
+ }
+
+ private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
+ menuItemButton.setTag(menuItem);
+ menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
+ }
+
+ /**
+ * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
+ * animations. See comment about this in the code.
+ */
+ private int getAdjustedDuration(int originalDuration) {
+ if (mTransitionDurationScale < 150) {
+ // For smaller transition, decrease the time.
+ return Math.max(originalDuration - 50, 0);
+ } else if (mTransitionDurationScale > 300) {
+ // For bigger transition, increase the time.
+ return originalDuration + 50;
+ }
+
+ // Scale the animation duration with getDurationScale(). This allows
+ // android.view.animation.* animations to scale just like android.animation.* animations
+ // when animator duration scale is adjusted in "Developer Options".
+ // For this reason, do not use this method for android.animation.* animations.
+ return (int) (originalDuration * ValueAnimator.getDurationScale());
+ }
+
+ private void maybeComputeTransitionDurationScale() {
+ if (mMainPanelSize != null && mOverflowPanelSize != null) {
+ int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
+ int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
+ mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
+ mContentContainer.getContext().getResources().getDisplayMetrics().density);
+ }
+ }
+
+ private ViewGroup createMainPanel() {
+ ViewGroup mainPanel = new LinearLayout(mContext) {
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (isOverflowAnimating()) {
+ // Update widthMeasureSpec to make sure that this view is not clipped
+ // as we offset it's coordinates with respect to it's parent.
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ mMainPanelSize.getWidth(),
+ MeasureSpec.EXACTLY);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ // Intercept the touch event while the overflow is animating.
+ return isOverflowAnimating();
+ }
+ };
+ return mainPanel;
+ }
+
+ private ImageButton createOverflowButton() {
+ final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
+ .inflate(R.layout.floating_popup_overflow_button, null);
+ overflowButton.setImageDrawable(mOverflow);
+ overflowButton.setOnClickListener(v -> {
+ if (mIsOverflowOpen) {
+ overflowButton.setImageDrawable(mToOverflow);
+ mToOverflow.start();
+ closeOverflow();
+ } else {
+ overflowButton.setImageDrawable(mToArrow);
+ mToArrow.start();
+ openOverflow();
+ }
+ });
+ return overflowButton;
+ }
+
+ private OverflowPanel createOverflowPanel() {
+ final OverflowPanel overflowPanel = new OverflowPanel(this);
+ overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ overflowPanel.setDivider(null);
+ overflowPanel.setDividerHeight(0);
+
+ final ArrayAdapter adapter =
+ new ArrayAdapter<MenuItem>(mContext, 0) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return mOverflowPanelViewHelper.getView(
+ getItem(position), mOverflowPanelSize.getWidth(), convertView);
+ }
+ };
+ overflowPanel.setAdapter(adapter);
+
+ overflowPanel.setOnItemClickListener((parent, view, position, id) -> {
+ MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
+ if (mOnMenuItemClickListener != null) {
+ mOnMenuItemClickListener.onMenuItemClick(menuItem);
+ }
+ });
+
+ return overflowPanel;
+ }
+
+ private boolean isOverflowAnimating() {
+ final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
+ && !mOpenOverflowAnimation.hasEnded();
+ final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
+ && !mCloseOverflowAnimation.hasEnded();
+ return overflowOpening || overflowClosing;
+ }
+
+ private Animation.AnimationListener createOverflowAnimationListener() {
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ // Disable the overflow button while it's animating.
+ // It will be re-enabled when the animation stops.
+ mOverflowButton.setEnabled(false);
+ // Ensure both panels have visibility turned on when the overflow animation
+ // starts.
+ mMainPanel.setVisibility(View.VISIBLE);
+ mOverflowPanel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // Posting this because it seems like this is called before the animation
+ // actually ends.
+ mContentContainer.post(() -> {
+ setPanelsStatesAtRestingPosition();
+ setContentAreaAsTouchableSurface();
+ });
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ };
+ return listener;
+ }
+
+ private static Size measure(View view) {
+ Preconditions.checkState(view.getParent() == null);
+ view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
+ }
+
+ private static void setSize(View view, int width, int height) {
+ view.setMinimumWidth(width);
+ view.setMinimumHeight(height);
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
+ params.width = width;
+ params.height = height;
+ view.setLayoutParams(params);
+ }
+
+ private static void setSize(View view, Size size) {
+ setSize(view, size.getWidth(), size.getHeight());
+ }
+
+ private static void setWidth(View view, int width) {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ setSize(view, width, params.height);
+ }
+
+ private static void setHeight(View view, int height) {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ setSize(view, params.width, height);
+ }
+
+ /**
+ * A custom ListView for the overflow panel.
+ */
+ private static final class OverflowPanel extends ListView {
+
+ private final FloatingToolbarPopup mPopup;
+
+ OverflowPanel(FloatingToolbarPopup popup) {
+ super(Objects.requireNonNull(popup).mContext);
+ this.mPopup = popup;
+ setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
+ setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Update heightMeasureSpec to make sure that this view is not clipped
+ // as we offset it's coordinates with respect to it's parent.
+ int height = mPopup.mOverflowPanelSize.getHeight()
+ - mPopup.mOverflowButtonSize.getHeight();
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mPopup.isOverflowAnimating()) {
+ // Eat the touch event.
+ return true;
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ protected boolean awakenScrollBars() {
+ return super.awakenScrollBars();
+ }
+ }
+
+ /**
+ * A custom interpolator used for various floating toolbar animations.
+ */
+ private static final class LogAccelerateInterpolator implements Interpolator {
+
+ private static final int BASE = 100;
+ private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
+
+ private static float computeLog(float t, int base) {
+ return (float) (1 - Math.pow(base, -t));
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
+ }
+ }
+
+ /**
+ * A helper for generating views for the overflow panel.
+ */
+ private static final class OverflowPanelViewHelper {
+
+ private final View mCalculator;
+ private final int mIconTextSpacing;
+ private final int mSidePadding;
+
+ private final Context mContext;
+
+ public OverflowPanelViewHelper(Context context, int iconTextSpacing) {
+ mContext = Objects.requireNonNull(context);
+ mIconTextSpacing = iconTextSpacing;
+ mSidePadding = context.getResources()
+ .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
+ mCalculator = createMenuButton(null);
+ }
+
+ public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
+ Objects.requireNonNull(menuItem);
+ if (convertView != null) {
+ updateMenuItemButton(
+ convertView, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
+ } else {
+ convertView = createMenuButton(menuItem);
+ }
+ convertView.setMinimumWidth(minimumWidth);
+ return convertView;
+ }
+
+ public int calculateWidth(MenuItem menuItem) {
+ updateMenuItemButton(
+ mCalculator, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
+ mCalculator.measure(
+ View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ return mCalculator.getMeasuredWidth();
+ }
+
+ private View createMenuButton(MenuItem menuItem) {
+ View button = createMenuItemButton(
+ mContext, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
+ button.setPadding(mSidePadding, 0, mSidePadding, 0);
+ return button;
+ }
+
+ private boolean shouldShowIcon(MenuItem menuItem) {
+ if (menuItem != null) {
+ return menuItem.getGroupId() == android.R.id.textAssist;
+ }
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a menu button for the specified menu item.
+ */
+ private static View createMenuItemButton(
+ Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
+ final View menuItemButton = LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_menu_button, null);
+ if (menuItem != null) {
+ updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon);
+ }
+ return menuItemButton;
+ }
+
+ /**
+ * Updates the specified menu item button with the specified menu item data.
+ */
+ private static void updateMenuItemButton(
+ View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
+ final TextView buttonText = menuItemButton.findViewById(
+ R.id.floating_toolbar_menu_item_text);
+ buttonText.setEllipsize(null);
+ if (TextUtils.isEmpty(menuItem.getTitle())) {
+ buttonText.setVisibility(View.GONE);
+ } else {
+ buttonText.setVisibility(View.VISIBLE);
+ buttonText.setText(menuItem.getTitle());
+ }
+ final ImageView buttonIcon = menuItemButton.findViewById(
+ R.id.floating_toolbar_menu_item_image);
+ if (menuItem.getIcon() == null || !showIcon) {
+ buttonIcon.setVisibility(View.GONE);
+ if (buttonText != null) {
+ buttonText.setPaddingRelative(0, 0, 0, 0);
+ }
+ } else {
+ buttonIcon.setVisibility(View.VISIBLE);
+ buttonIcon.setImageDrawable(menuItem.getIcon());
+ if (buttonText != null) {
+ buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
+ }
+ }
+ final CharSequence contentDescription = menuItem.getContentDescription();
+ if (TextUtils.isEmpty(contentDescription)) {
+ menuItemButton.setContentDescription(menuItem.getTitle());
+ } else {
+ menuItemButton.setContentDescription(contentDescription);
+ }
+ }
+
+ private static ViewGroup createContentContainer(Context context) {
+ ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
+ .inflate(R.layout.floating_popup_container, null);
+ contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ contentContainer.setTag(FLOATING_TOOLBAR_TAG);
+ contentContainer.setClipToOutline(true);
+ return contentContainer;
+ }
+
+ private static PopupWindow createPopupWindow(ViewGroup content) {
+ ViewGroup popupContentHolder = new LinearLayout(content.getContext());
+ PopupWindow popupWindow = new PopupWindow(popupContentHolder);
+ // TODO: Use .setIsLaidOutInScreen(true) instead of .setClippingEnabled(false)
+ // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
+ popupWindow.setClippingEnabled(false);
+ popupWindow.setWindowLayoutType(
+ WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
+ popupWindow.setAnimationStyle(0);
+ popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ content.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ popupContentHolder.addView(content);
+ return popupWindow;
+ }
+
+ /**
+ * Creates an "appear" animation for the specified view.
+ *
+ * @param view The view to animate
+ */
+ private static AnimatorSet createEnterAnimation(View view) {
+ AnimatorSet animation = new AnimatorSet();
+ animation.playTogether(
+ ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
+ return animation;
+ }
+
+ /**
+ * Creates a "disappear" animation for the specified view.
+ *
+ * @param view The view to animate
+ * @param startDelay The start delay of the animation
+ * @param listener The animation listener
+ */
+ private static AnimatorSet createExitAnimation(
+ View view, int startDelay, Animator.AnimatorListener listener) {
+ AnimatorSet animation = new AnimatorSet();
+ animation.playTogether(
+ ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
+ animation.setStartDelay(startDelay);
+ animation.addListener(listener);
+ return animation;
+ }
+
+ /**
+ * Returns a re-themed context with controlled look and feel for views.
+ */
+ private static Context applyDefaultTheme(Context originalContext) {
+ TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
+ boolean isLightTheme = a.getBoolean(0, true);
+ int themeId
+ = isLightTheme ? R.style.Theme_DeviceDefault_Light : R.style.Theme_DeviceDefault;
+ a.recycle();
+ return new ContextThemeWrapper(originalContext, themeId);
+ }
+}
diff --git a/com/android/internal/widget/GapWorker.java b/com/android/internal/widget/GapWorker.java
new file mode 100644
index 0000000..5972396
--- /dev/null
+++ b/com/android/internal/widget/GapWorker.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.os.Trace;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.concurrent.TimeUnit;
+
+final class GapWorker implements Runnable {
+
+ static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
+
+ ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();
+ long mPostTimeNs;
+ long mFrameIntervalNs;
+
+ static class Task {
+ public boolean immediate;
+ public int viewVelocity;
+ public int distanceToItem;
+ public RecyclerView view;
+ public int position;
+
+ public void clear() {
+ immediate = false;
+ viewVelocity = 0;
+ distanceToItem = 0;
+ view = null;
+ position = 0;
+ }
+ }
+
+ /**
+ * Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects
+ * are pooled in the ArrayList, and never removed to avoid allocations, but always cleared
+ * in between calls.
+ */
+ private ArrayList<Task> mTasks = new ArrayList<>();
+
+ /**
+ * Prefetch information associated with a specific RecyclerView.
+ */
+ static class LayoutPrefetchRegistryImpl
+ implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {
+ int mPrefetchDx;
+ int mPrefetchDy;
+ int[] mPrefetchArray;
+
+ int mCount;
+
+ void setPrefetchVector(int dx, int dy) {
+ mPrefetchDx = dx;
+ mPrefetchDy = dy;
+ }
+
+ void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
+ mCount = 0;
+ if (mPrefetchArray != null) {
+ Arrays.fill(mPrefetchArray, -1);
+ }
+
+ final RecyclerView.LayoutManager layout = view.mLayout;
+ if (view.mAdapter != null
+ && layout != null
+ && layout.isItemPrefetchEnabled()) {
+ if (nested) {
+ // nested prefetch, only if no adapter updates pending. Note: we don't query
+ // view.hasPendingAdapterUpdates(), as first layout may not have occurred
+ if (!view.mAdapterHelper.hasPendingUpdates()) {
+ layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
+ }
+ } else {
+ // momentum based prefetch, only if we trust current child/adapter state
+ if (!view.hasPendingAdapterUpdates()) {
+ layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
+ view.mState, this);
+ }
+ }
+
+ if (mCount > layout.mPrefetchMaxCountObserved) {
+ layout.mPrefetchMaxCountObserved = mCount;
+ layout.mPrefetchMaxObservedInInitialPrefetch = nested;
+ view.mRecycler.updateViewCacheSize();
+ }
+ }
+ }
+
+ @Override
+ public void addPosition(int layoutPosition, int pixelDistance) {
+ if (pixelDistance < 0) {
+ throw new IllegalArgumentException("Pixel distance must be non-negative");
+ }
+
+ // allocate or expand array as needed, doubling when needed
+ final int storagePosition = mCount * 2;
+ if (mPrefetchArray == null) {
+ mPrefetchArray = new int[4];
+ Arrays.fill(mPrefetchArray, -1);
+ } else if (storagePosition >= mPrefetchArray.length) {
+ final int[] oldArray = mPrefetchArray;
+ mPrefetchArray = new int[storagePosition * 2];
+ System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
+ }
+
+ // add position
+ mPrefetchArray[storagePosition] = layoutPosition;
+ mPrefetchArray[storagePosition + 1] = pixelDistance;
+
+ mCount++;
+ }
+
+ boolean lastPrefetchIncludedPosition(int position) {
+ if (mPrefetchArray != null) {
+ final int count = mCount * 2;
+ for (int i = 0; i < count; i += 2) {
+ if (mPrefetchArray[i] == position) return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called when prefetch indices are no longer valid for cache prioritization.
+ */
+ void clearPrefetchPositions() {
+ if (mPrefetchArray != null) {
+ Arrays.fill(mPrefetchArray, -1);
+ }
+ }
+ }
+
+ public void add(RecyclerView recyclerView) {
+ if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) {
+ throw new IllegalStateException("RecyclerView already present in worker list!");
+ }
+ mRecyclerViews.add(recyclerView);
+ }
+
+ public void remove(RecyclerView recyclerView) {
+ boolean removeSuccess = mRecyclerViews.remove(recyclerView);
+ if (RecyclerView.DEBUG && !removeSuccess) {
+ throw new IllegalStateException("RecyclerView removal failed!");
+ }
+ }
+
+ /**
+ * Schedule a prefetch immediately after the current traversal.
+ */
+ void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
+ if (recyclerView.isAttachedToWindow()) {
+ if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
+ throw new IllegalStateException("attempting to post unregistered view!");
+ }
+ if (mPostTimeNs == 0) {
+ mPostTimeNs = recyclerView.getNanoTime();
+ recyclerView.post(this);
+ }
+ }
+
+ recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
+ }
+
+ static Comparator<Task> sTaskComparator = new Comparator<Task>() {
+ @Override
+ public int compare(Task lhs, Task rhs) {
+ // first, prioritize non-cleared tasks
+ if ((lhs.view == null) != (rhs.view == null)) {
+ return lhs.view == null ? 1 : -1;
+ }
+
+ // then prioritize immediate
+ if (lhs.immediate != rhs.immediate) {
+ return lhs.immediate ? -1 : 1;
+ }
+
+ // then prioritize _highest_ view velocity
+ int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity;
+ if (deltaViewVelocity != 0) return deltaViewVelocity;
+
+ // then prioritize _lowest_ distance to item
+ int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem;
+ if (deltaDistanceToItem != 0) return deltaDistanceToItem;
+
+ return 0;
+ }
+ };
+
+ private void buildTaskList() {
+ // Update PrefetchRegistry in each view
+ final int viewCount = mRecyclerViews.size();
+ int totalTaskCount = 0;
+ for (int i = 0; i < viewCount; i++) {
+ RecyclerView view = mRecyclerViews.get(i);
+ view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
+ totalTaskCount += view.mPrefetchRegistry.mCount;
+ }
+
+ // Populate task list from prefetch data...
+ mTasks.ensureCapacity(totalTaskCount);
+ int totalTaskIndex = 0;
+ for (int i = 0; i < viewCount; i++) {
+ RecyclerView view = mRecyclerViews.get(i);
+ LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
+ final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
+ + Math.abs(prefetchRegistry.mPrefetchDy);
+ for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
+ final Task task;
+ if (totalTaskIndex >= mTasks.size()) {
+ task = new Task();
+ mTasks.add(task);
+ } else {
+ task = mTasks.get(totalTaskIndex);
+ }
+ final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
+
+ task.immediate = distanceToItem <= viewVelocity;
+ task.viewVelocity = viewVelocity;
+ task.distanceToItem = distanceToItem;
+ task.view = view;
+ task.position = prefetchRegistry.mPrefetchArray[j];
+
+ totalTaskIndex++;
+ }
+ }
+
+ // ... and priority sort
+ Collections.sort(mTasks, sTaskComparator);
+ }
+
+ static boolean isPrefetchPositionAttached(RecyclerView view, int position) {
+ final int childCount = view.mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View attachedView = view.mChildHelper.getUnfilteredChildAt(i);
+ RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView);
+ // Note: can use mPosition here because adapter doesn't have pending updates
+ if (holder.mPosition == position && !holder.isInvalid()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
+ int position, long deadlineNs) {
+ if (isPrefetchPositionAttached(view, position)) {
+ // don't attempt to prefetch attached views
+ return null;
+ }
+
+ RecyclerView.Recycler recycler = view.mRecycler;
+ RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
+ position, false, deadlineNs);
+
+ if (holder != null) {
+ if (holder.isBound()) {
+ // Only give the view a chance to go into the cache if binding succeeded
+ // Note that we must use public method, since item may need cleanup
+ recycler.recycleView(holder.itemView);
+ } else {
+ // Didn't bind, so we can't cache the view, but it will stay in the pool until
+ // next prefetch/traversal. If a View fails to bind, it means we didn't have
+ // enough time prior to the deadline (and won't for other instances of this
+ // type, during this GapWorker prefetch pass).
+ recycler.addViewHolderToRecycledViewPool(holder, false);
+ }
+ }
+ return holder;
+ }
+
+ private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView,
+ long deadlineNs) {
+ if (innerView == null) {
+ return;
+ }
+
+ if (innerView.mDataSetHasChangedAfterLayout
+ && innerView.mChildHelper.getUnfilteredChildCount() != 0) {
+ // RecyclerView has new data, but old attached views. Clear everything, so that
+ // we can prefetch without partially stale data.
+ innerView.removeAndRecycleViews();
+ }
+
+ // do nested prefetch!
+ final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry;
+ innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true);
+
+ if (innerPrefetchRegistry.mCount != 0) {
+ try {
+ Trace.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG);
+ innerView.mState.prepareForNestedPrefetch(innerView.mAdapter);
+ for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) {
+ // Note that we ignore immediate flag for inner items because
+ // we have lower confidence they're needed next frame.
+ final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i];
+ prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs);
+ }
+ } finally {
+ Trace.endSection();
+ }
+ }
+ }
+
+ private void flushTaskWithDeadline(Task task, long deadlineNs) {
+ long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
+ RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
+ task.position, taskDeadlineNs);
+ if (holder != null && holder.mNestedRecyclerView != null) {
+ prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
+ }
+ }
+
+ private void flushTasksWithDeadline(long deadlineNs) {
+ for (int i = 0; i < mTasks.size(); i++) {
+ final Task task = mTasks.get(i);
+ if (task.view == null) {
+ break; // done with populated tasks
+ }
+ flushTaskWithDeadline(task, deadlineNs);
+ task.clear();
+ }
+ }
+
+ void prefetch(long deadlineNs) {
+ buildTaskList();
+ flushTasksWithDeadline(deadlineNs);
+ }
+
+ @Override
+ public void run() {
+ try {
+ Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
+
+ if (mRecyclerViews.isEmpty()) {
+ // abort - no work to do
+ return;
+ }
+
+ // Query last vsync so we can predict next one. Note that drawing time not yet
+ // valid in animation/input callbacks, so query it here to be safe.
+ long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
+ mRecyclerViews.get(0).getDrawingTime());
+ if (lastFrameVsyncNs == 0) {
+ // abort - couldn't get last vsync for estimating next
+ return;
+ }
+
+ // TODO: consider rebasing deadline if frame was already dropped due to long UI work.
+ // Next frame will still wait for VSYNC, so we can still use the gap if it exists.
+ long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;
+
+ prefetch(nextFrameNs);
+
+ // TODO: consider rescheduling self, if there's more work to do
+ } finally {
+ mPostTimeNs = 0;
+ Trace.endSection();
+ }
+ }
+}
diff --git a/com/android/internal/widget/GridLayoutManager.java b/com/android/internal/widget/GridLayoutManager.java
new file mode 100644
index 0000000..e0502f1
--- /dev/null
+++ b/com/android/internal/widget/GridLayoutManager.java
@@ -0,0 +1,1065 @@
+/*
+ * Copyright (C) 2019 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.Arrays;
+
+/**
+ * Note: This GridLayoutManager widget may lack of latest fix because it is ported from
+ * oc-dr1-release version of android.support.v7.widget.GridLayoutManager due to compatibility
+ * concern with other internal widgets, like {@link RecyclerView} and {@link LinearLayoutManager},
+ * and is merely used for {@link com.android.internal.app.ChooserActivity}.
+ *
+ * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
+ * <p>
+ * By default, each item occupies 1 span. You can change it by providing a custom
+ * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
+ */
+public class GridLayoutManager extends LinearLayoutManager {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "GridLayoutManager";
+ public static final int DEFAULT_SPAN_COUNT = -1;
+ /**
+ * Span size have been changed but we've not done a new layout calculation.
+ */
+ boolean mPendingSpanCountChange = false;
+ int mSpanCount = DEFAULT_SPAN_COUNT;
+ /**
+ * Right borders for each span.
+ * <p>For <b>i-th</b> item start is {@link #mCachedBorders}[i-1] + 1
+ * and end is {@link #mCachedBorders}[i].
+ */
+ int[] mCachedBorders;
+ /**
+ * Temporary array to keep views in layoutChunk method
+ */
+ View[] mSet;
+ final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
+ final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
+ SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
+ // re-used variable to acquire decor insets from RecyclerView
+ final Rect mDecorInsets = new Rect();
+
+ /**
+ * Constructor used when layout manager is set in XML by RecyclerView attribute
+ * "layoutManager". If spanCount is not specified in the XML, it defaults to a
+ * single column.
+ *
+ */
+ public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
+ setSpanCount(properties.spanCount);
+ }
+
+ /**
+ * Creates a vertical GridLayoutManager
+ *
+ * @param context Current context, will be used to access resources.
+ * @param spanCount The number of columns in the grid
+ */
+ public GridLayoutManager(Context context, int spanCount) {
+ super(context);
+ setSpanCount(spanCount);
+ }
+
+ /**
+ * @param context Current context, will be used to access resources.
+ * @param spanCount The number of columns or rows in the grid
+ * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
+ * #VERTICAL}.
+ * @param reverseLayout When set to true, layouts from end to start.
+ */
+ public GridLayoutManager(Context context, int spanCount, int orientation,
+ boolean reverseLayout) {
+ super(context, orientation, reverseLayout);
+ setSpanCount(spanCount);
+ }
+
+ /**
+ * stackFromEnd is not supported by GridLayoutManager. Consider using
+ * {@link #setReverseLayout(boolean)}.
+ */
+ @Override
+ public void setStackFromEnd(boolean stackFromEnd) {
+ if (stackFromEnd) {
+ throw new UnsupportedOperationException(
+ "GridLayoutManager does not support stack from end."
+ + " Consider using reverse layout");
+ }
+ super.setStackFromEnd(false);
+ }
+
+ @Override
+ public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (mOrientation == HORIZONTAL) {
+ return mSpanCount;
+ }
+ if (state.getItemCount() < 1) {
+ return 0;
+ }
+ // Row count is one more than the last item's row index.
+ return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1;
+ }
+
+ @Override
+ public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (mOrientation == VERTICAL) {
+ return mSpanCount;
+ }
+ if (state.getItemCount() < 1) {
+ return 0;
+ }
+ // Column count is one more than the last item's column index.
+ return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
+ RecyclerView.State state, View host, AccessibilityNodeInfo info) {
+ ViewGroup.LayoutParams lp = host.getLayoutParams();
+ if (!(lp instanceof LayoutParams)) {
+ super.onInitializeAccessibilityNodeInfoForItem(host, info);
+ return;
+ }
+ LayoutParams glp = (LayoutParams) lp;
+ int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition());
+ if (mOrientation == HORIZONTAL) {
+ info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(
+ glp.getSpanIndex(), glp.getSpanSize(),
+ spanGroupIndex, 1,
+ mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
+ } else { // VERTICAL
+ info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(
+ spanGroupIndex, 1,
+ glp.getSpanIndex(), glp.getSpanSize(),
+ mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
+ }
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (state.isPreLayout()) {
+ cachePreLayoutSpanMapping();
+ }
+ super.onLayoutChildren(recycler, state);
+ if (DEBUG) {
+ validateChildOrder();
+ }
+ clearPreLayoutSpanMappingCache();
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ super.onLayoutCompleted(state);
+ mPendingSpanCountChange = false;
+ }
+
+ private void clearPreLayoutSpanMappingCache() {
+ mPreLayoutSpanSizeCache.clear();
+ mPreLayoutSpanIndexCache.clear();
+ }
+
+ private void cachePreLayoutSpanMapping() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+ final int viewPosition = lp.getViewLayoutPosition();
+ mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
+ mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
+ }
+ }
+
+ @Override
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ mSpanSizeLookup.invalidateSpanIndexCache();
+ }
+
+ @Override
+ public void onItemsChanged(RecyclerView recyclerView) {
+ mSpanSizeLookup.invalidateSpanIndexCache();
+ }
+
+ @Override
+ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+ mSpanSizeLookup.invalidateSpanIndexCache();
+ }
+
+ @Override
+ public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
+ Object payload) {
+ mSpanSizeLookup.invalidateSpanIndexCache();
+ }
+
+ @Override
+ public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
+ mSpanSizeLookup.invalidateSpanIndexCache();
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ if (mOrientation == HORIZONTAL) {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ } else {
+ return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
+ return new LayoutParams(c, attrs);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp instanceof ViewGroup.MarginLayoutParams) {
+ return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
+ } else {
+ return new LayoutParams(lp);
+ }
+ }
+
+ @Override
+ public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
+ return lp instanceof LayoutParams;
+ }
+
+ /**
+ * Sets the source to get the number of spans occupied by each item in the adapter.
+ *
+ * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
+ * occupied by each item
+ */
+ public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
+ mSpanSizeLookup = spanSizeLookup;
+ }
+
+ /**
+ * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
+ *
+ * @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
+ */
+ public SpanSizeLookup getSpanSizeLookup() {
+ return mSpanSizeLookup;
+ }
+
+ private void updateMeasurements() {
+ int totalSpace;
+ if (getOrientation() == VERTICAL) {
+ totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
+ } else {
+ totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
+ }
+ calculateItemBorders(totalSpace);
+ }
+
+ @Override
+ public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
+ if (mCachedBorders == null) {
+ super.setMeasuredDimension(childrenBounds, wSpec, hSpec);
+ }
+ final int width, height;
+ final int horizontalPadding = getPaddingLeft() + getPaddingRight();
+ final int verticalPadding = getPaddingTop() + getPaddingBottom();
+ if (mOrientation == VERTICAL) {
+ final int usedHeight = childrenBounds.height() + verticalPadding;
+ height = chooseSize(hSpec, usedHeight, getMinimumHeight());
+ width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1] + horizontalPadding,
+ getMinimumWidth());
+ } else {
+ final int usedWidth = childrenBounds.width() + horizontalPadding;
+ width = chooseSize(wSpec, usedWidth, getMinimumWidth());
+ height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1] + verticalPadding,
+ getMinimumHeight());
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * @param totalSpace Total available space after padding is removed
+ */
+ private void calculateItemBorders(int totalSpace) {
+ mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace);
+ }
+
+ /**
+ * @param cachedBorders The out array
+ * @param spanCount number of spans
+ * @param totalSpace total available space after padding is removed
+ * @return The updated array. Might be the same instance as the provided array if its size
+ * has not changed.
+ */
+ static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
+ if (cachedBorders == null || cachedBorders.length != spanCount + 1
+ || cachedBorders[cachedBorders.length - 1] != totalSpace) {
+ cachedBorders = new int[spanCount + 1];
+ }
+ cachedBorders[0] = 0;
+ int sizePerSpan = totalSpace / spanCount;
+ int sizePerSpanRemainder = totalSpace % spanCount;
+ int consumedPixels = 0;
+ int additionalSize = 0;
+ for (int i = 1; i <= spanCount; i++) {
+ int itemSize = sizePerSpan;
+ additionalSize += sizePerSpanRemainder;
+ if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
+ itemSize += 1;
+ additionalSize -= spanCount;
+ }
+ consumedPixels += itemSize;
+ cachedBorders[i] = consumedPixels;
+ }
+ return cachedBorders;
+ }
+
+ int getSpaceForSpanRange(int startSpan, int spanSize) {
+ if (mOrientation == VERTICAL && isLayoutRTL()) {
+ return mCachedBorders[mSpanCount - startSpan]
+ - mCachedBorders[mSpanCount - startSpan - spanSize];
+ } else {
+ return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan];
+ }
+ }
+
+ @Override
+ void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state,
+ AnchorInfo anchorInfo, int itemDirection) {
+ super.onAnchorReady(recycler, state, anchorInfo, itemDirection);
+ updateMeasurements();
+ if (state.getItemCount() > 0 && !state.isPreLayout()) {
+ ensureAnchorIsInCorrectSpan(recycler, state, anchorInfo, itemDirection);
+ }
+ ensureViewSet();
+ }
+
+ private void ensureViewSet() {
+ if (mSet == null || mSet.length != mSpanCount) {
+ mSet = new View[mSpanCount];
+ }
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ updateMeasurements();
+ ensureViewSet();
+ return super.scrollHorizontallyBy(dx, recycler, state);
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ updateMeasurements();
+ ensureViewSet();
+ return super.scrollVerticallyBy(dy, recycler, state);
+ }
+
+ private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler,
+ RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) {
+ final boolean layingOutInPrimaryDirection =
+ itemDirection == LayoutState.ITEM_DIRECTION_TAIL;
+ int span = getSpanIndex(recycler, state, anchorInfo.mPosition);
+ if (layingOutInPrimaryDirection) {
+ // choose span 0
+ while (span > 0 && anchorInfo.mPosition > 0) {
+ anchorInfo.mPosition--;
+ span = getSpanIndex(recycler, state, anchorInfo.mPosition);
+ }
+ } else {
+ // choose the max span we can get. hopefully last one
+ final int indexLimit = state.getItemCount() - 1;
+ int pos = anchorInfo.mPosition;
+ int bestSpan = span;
+ while (pos < indexLimit) {
+ int next = getSpanIndex(recycler, state, pos + 1);
+ if (next > bestSpan) {
+ pos += 1;
+ bestSpan = next;
+ } else {
+ break;
+ }
+ }
+ anchorInfo.mPosition = pos;
+ }
+ }
+
+ @Override
+ View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
+ int start, int end, int itemCount) {
+ ensureLayoutState();
+ View invalidMatch = null;
+ View outOfBoundsMatch = null;
+ final int boundsStart = mOrientationHelper.getStartAfterPadding();
+ final int boundsEnd = mOrientationHelper.getEndAfterPadding();
+ final int diff = end > start ? 1 : -1;
+ for (int i = start; i != end; i += diff) {
+ final View view = getChildAt(i);
+ final int position = getPosition(view);
+ if (position >= 0 && position < itemCount) {
+ final int span = getSpanIndex(recycler, state, position);
+ if (span != 0) {
+ continue;
+ }
+ if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
+ if (invalidMatch == null) {
+ invalidMatch = view; // removed item, least preferred
+ }
+ } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd
+ || mOrientationHelper.getDecoratedEnd(view) < boundsStart) {
+ if (outOfBoundsMatch == null) {
+ outOfBoundsMatch = view; // item is not visible, less preferred
+ }
+ } else {
+ return view;
+ }
+ }
+ }
+ return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
+ }
+
+ private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
+ int viewPosition) {
+ if (!state.isPreLayout()) {
+ return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
+ }
+ final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
+ if (adapterPosition == -1) {
+ if (DEBUG) {
+ throw new RuntimeException("Cannot find span group index for position "
+ + viewPosition);
+ }
+ Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
+ return 0;
+ }
+ return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
+ }
+
+ private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
+ if (!state.isPreLayout()) {
+ return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
+ }
+ final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
+ if (cached != -1) {
+ return cached;
+ }
+ final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
+ if (adapterPosition == -1) {
+ if (DEBUG) {
+ throw new RuntimeException("Cannot find span index for pre layout position. It is"
+ + " not cached, not in the adapter. Pos:" + pos);
+ }
+ Log.w(TAG, "Cannot find span size for pre layout position. It is"
+ + " not cached, not in the adapter. Pos:" + pos);
+ return 0;
+ }
+ return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
+ }
+
+ private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
+ if (!state.isPreLayout()) {
+ return mSpanSizeLookup.getSpanSize(pos);
+ }
+ final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
+ if (cached != -1) {
+ return cached;
+ }
+ final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
+ if (adapterPosition == -1) {
+ if (DEBUG) {
+ throw new RuntimeException("Cannot find span size for pre layout position. It is"
+ + " not cached, not in the adapter. Pos:" + pos);
+ }
+ Log.w(TAG, "Cannot find span size for pre layout position. It is"
+ + " not cached, not in the adapter. Pos:" + pos);
+ return 1;
+ }
+ return mSpanSizeLookup.getSpanSize(adapterPosition);
+ }
+
+ @Override
+ void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ int remainingSpan = mSpanCount;
+ int count = 0;
+ while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
+ final int pos = layoutState.mCurrentPosition;
+ layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
+ final int spanSize = mSpanSizeLookup.getSpanSize(pos);
+ remainingSpan -= spanSize;
+ layoutState.mCurrentPosition += layoutState.mItemDirection;
+ count++;
+ }
+ }
+
+ @Override
+ void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
+ LayoutState layoutState, LayoutChunkResult result) {
+ final int otherDirSpecMode = mOrientationHelper.getModeInOther();
+ final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY;
+ final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0;
+ // if grid layout's dimensions are not specified, let the new row change the measurements
+ // This is not perfect since we not covering all rows but still solves an important case
+ // where they may have a header row which should be laid out according to children.
+ if (flexibleInOtherDir) {
+ updateMeasurements(); // reset measurements
+ }
+ final boolean layingOutInPrimaryDirection =
+ layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
+ int count = 0;
+ int consumedSpanCount = 0;
+ int remainingSpan = mSpanCount;
+ if (!layingOutInPrimaryDirection) {
+ int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
+ int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
+ remainingSpan = itemSpanIndex + itemSpanSize;
+ }
+ while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
+ int pos = layoutState.mCurrentPosition;
+ final int spanSize = getSpanSize(recycler, state, pos);
+ if (spanSize > mSpanCount) {
+ throw new IllegalArgumentException("Item at position " + pos + " requires "
+ + spanSize + " spans but GridLayoutManager has only " + mSpanCount
+ + " spans.");
+ }
+ remainingSpan -= spanSize;
+ if (remainingSpan < 0) {
+ break; // item did not fit into this row or column
+ }
+ View view = layoutState.next(recycler);
+ if (view == null) {
+ break;
+ }
+ consumedSpanCount += spanSize;
+ mSet[count] = view;
+ count++;
+ }
+ if (count == 0) {
+ result.mFinished = true;
+ return;
+ }
+ int maxSize = 0;
+ float maxSizeInOther = 0; // use a float to get size per span
+ // we should assign spans before item decor offsets are calculated
+ assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
+ for (int i = 0; i < count; i++) {
+ View view = mSet[i];
+ if (layoutState.mScrapList == null) {
+ if (layingOutInPrimaryDirection) {
+ addView(view);
+ } else {
+ addView(view, 0);
+ }
+ } else {
+ if (layingOutInPrimaryDirection) {
+ addDisappearingView(view);
+ } else {
+ addDisappearingView(view, 0);
+ }
+ }
+ calculateItemDecorationsForChild(view, mDecorInsets);
+ measureChild(view, otherDirSpecMode, false);
+ final int size = mOrientationHelper.getDecoratedMeasurement(view);
+ if (size > maxSize) {
+ maxSize = size;
+ }
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view)
+ / lp.mSpanSize;
+ if (otherSize > maxSizeInOther) {
+ maxSizeInOther = otherSize;
+ }
+ }
+ if (flexibleInOtherDir) {
+ // re-distribute columns
+ guessMeasurement(maxSizeInOther, currentOtherDirSize);
+ // now we should re-measure any item that was match parent.
+ maxSize = 0;
+ for (int i = 0; i < count; i++) {
+ View view = mSet[i];
+ measureChild(view, View.MeasureSpec.EXACTLY, true);
+ final int size = mOrientationHelper.getDecoratedMeasurement(view);
+ if (size > maxSize) {
+ maxSize = size;
+ }
+ }
+ }
+ // Views that did not measure the maxSize has to be re-measured
+ // We will stop doing this once we introduce Gravity in the GLM layout params
+ for (int i = 0; i < count; i++) {
+ final View view = mSet[i];
+ if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final Rect decorInsets = lp.mDecorInsets;
+ final int verticalInsets = decorInsets.top + decorInsets.bottom
+ + lp.topMargin + lp.bottomMargin;
+ final int horizontalInsets = decorInsets.left + decorInsets.right
+ + lp.leftMargin + lp.rightMargin;
+ final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
+ final int wSpec;
+ final int hSpec;
+ if (mOrientation == VERTICAL) {
+ wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
+ horizontalInsets, lp.width, false);
+ hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
+ View.MeasureSpec.EXACTLY);
+ } else {
+ wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets,
+ View.MeasureSpec.EXACTLY);
+ hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
+ verticalInsets, lp.height, false);
+ }
+ measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
+ }
+ }
+ result.mConsumed = maxSize;
+ int left = 0, right = 0, top = 0, bottom = 0;
+ if (mOrientation == VERTICAL) {
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ bottom = layoutState.mOffset;
+ top = bottom - maxSize;
+ } else {
+ top = layoutState.mOffset;
+ bottom = top + maxSize;
+ }
+ } else {
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ right = layoutState.mOffset;
+ left = right - maxSize;
+ } else {
+ left = layoutState.mOffset;
+ right = left + maxSize;
+ }
+ }
+ for (int i = 0; i < count; i++) {
+ View view = mSet[i];
+ LayoutParams params = (LayoutParams) view.getLayoutParams();
+ if (mOrientation == VERTICAL) {
+ if (isLayoutRTL()) {
+ right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
+ left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
+ } else {
+ left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
+ right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
+ }
+ } else {
+ top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
+ bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
+ }
+ // We calculate everything with View's bounding box (which includes decor and margins)
+ // To calculate correct layout position, we subtract margins.
+ layoutDecoratedWithMargins(view, left, top, right, bottom);
+ if (DEBUG) {
+ Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
+ + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
+ }
+ // Consume the available space if the view is not removed OR changed
+ if (params.isItemRemoved() || params.isItemChanged()) {
+ result.mIgnoreConsumed = true;
+ }
+ result.mFocusable |= view.hasFocusable();
+ }
+ Arrays.fill(mSet, null);
+ }
+
+ /**
+ * Measures a child with currently known information. This is not necessarily the child's final
+ * measurement. (see fillChunk for details).
+ *
+ * @param view The child view to be measured
+ * @param otherDirParentSpecMode The RV measure spec that should be used in the secondary
+ * orientation
+ * @param alreadyMeasured True if we've already measured this view once
+ */
+ private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) {
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final Rect decorInsets = lp.mDecorInsets;
+ final int verticalInsets = decorInsets.top + decorInsets.bottom
+ + lp.topMargin + lp.bottomMargin;
+ final int horizontalInsets = decorInsets.left + decorInsets.right
+ + lp.leftMargin + lp.rightMargin;
+ final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
+ final int wSpec;
+ final int hSpec;
+ if (mOrientation == VERTICAL) {
+ wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
+ horizontalInsets, lp.width, false);
+ hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
+ verticalInsets, lp.height, true);
+ } else {
+ hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
+ verticalInsets, lp.height, false);
+ wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(),
+ horizontalInsets, lp.width, true);
+ }
+ measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured);
+ }
+
+ /**
+ * This is called after laying out a row (if vertical) or a column (if horizontal) when the
+ * RecyclerView does not have exact measurement specs.
+ * <p>
+ * Here we try to assign a best guess width or height and re-do the layout to update other
+ * views that wanted to MATCH_PARENT in the non-scroll orientation.
+ *
+ * @param maxSizeInOther The maximum size per span ratio from the measurement of the
+ * children.
+ * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below.
+ */
+ private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) {
+ final int contentSize = Math.round(maxSizeInOther * mSpanCount);
+ // always re-calculate because borders were stretched during the fill
+ calculateItemBorders(Math.max(contentSize, currentOtherDirSize));
+ }
+
+ private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec,
+ boolean alreadyMeasured) {
+ RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
+ final boolean measure;
+ if (alreadyMeasured) {
+ measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp);
+ } else {
+ measure = shouldMeasureChild(child, widthSpec, heightSpec, lp);
+ }
+ if (measure) {
+ child.measure(widthSpec, heightSpec);
+ }
+ }
+
+ private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
+ int consumedSpanCount, boolean layingOutInPrimaryDirection) {
+ // spans are always assigned from 0 to N no matter if it is RTL or not.
+ // RTL is used only when positioning the view.
+ int span, start, end, diff;
+ // make sure we traverse from min position to max position
+ if (layingOutInPrimaryDirection) {
+ start = 0;
+ end = count;
+ diff = 1;
+ } else {
+ start = count - 1;
+ end = -1;
+ diff = -1;
+ }
+ span = 0;
+ for (int i = start; i != end; i += diff) {
+ View view = mSet[i];
+ LayoutParams params = (LayoutParams) view.getLayoutParams();
+ params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
+ params.mSpanIndex = span;
+ span += params.mSpanSize;
+ }
+ }
+
+ /**
+ * Returns the number of spans laid out by this grid.
+ *
+ * @return The number of spans
+ * @see #setSpanCount(int)
+ */
+ public int getSpanCount() {
+ return mSpanCount;
+ }
+
+ /**
+ * Sets the number of spans to be laid out.
+ * <p>
+ * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
+ * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
+ *
+ * @param spanCount The total number of spans in the grid
+ * @see #getSpanCount()
+ */
+ public void setSpanCount(int spanCount) {
+ if (spanCount == mSpanCount) {
+ return;
+ }
+ mPendingSpanCountChange = true;
+ if (spanCount < 1) {
+ throw new IllegalArgumentException("Span count should be at least 1. Provided "
+ + spanCount);
+ }
+ mSpanCount = spanCount;
+ mSpanSizeLookup.invalidateSpanIndexCache();
+ requestLayout();
+ }
+
+ /**
+ * A helper class to provide the number of spans each item occupies.
+ * <p>
+ * Default implementation sets each item to occupy exactly 1 span.
+ *
+ * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
+ */
+ public abstract static class SpanSizeLookup {
+ final SparseIntArray mSpanIndexCache = new SparseIntArray();
+ private boolean mCacheSpanIndices = false;
+
+ /**
+ * Returns the number of span occupied by the item at <code>position</code>.
+ *
+ * @param position The adapter position of the item
+ * @return The number of spans occupied by the item at the provided position
+ */
+ public abstract int getSpanSize(int position);
+
+ /**
+ * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
+ * not. By default these values are not cached. If you are not overriding
+ * {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
+ *
+ * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
+ */
+ public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
+ mCacheSpanIndices = cacheSpanIndices;
+ }
+
+ /**
+ * Clears the span index cache. GridLayoutManager automatically calls this method when
+ * adapter changes occur.
+ */
+ public void invalidateSpanIndexCache() {
+ mSpanIndexCache.clear();
+ }
+
+ /**
+ * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
+ *
+ * @return True if results of {@link #getSpanIndex(int, int)} are cached.
+ */
+ public boolean isSpanIndexCacheEnabled() {
+ return mCacheSpanIndices;
+ }
+
+ int getCachedSpanIndex(int position, int spanCount) {
+ if (!mCacheSpanIndices) {
+ return getSpanIndex(position, spanCount);
+ }
+ final int existing = mSpanIndexCache.get(position, -1);
+ if (existing != -1) {
+ return existing;
+ }
+ final int value = getSpanIndex(position, spanCount);
+ mSpanIndexCache.put(position, value);
+ return value;
+ }
+
+ /**
+ * Returns the final span index of the provided position.
+ * <p>
+ * If you have a faster way to calculate span index for your items, you should override
+ * this method. Otherwise, you should enable span index cache
+ * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
+ * disabled, default implementation traverses all items from 0 to
+ * <code>position</code>. When caching is enabled, it calculates from the closest cached
+ * value before the <code>position</code>.
+ * <p>
+ * If you override this method, you need to make sure it is consistent with
+ * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
+ * each item. It is called only for the reference item and rest of the items
+ * are assigned to spans based on the reference item. For example, you cannot assign a
+ * position to span 2 while span 1 is empty.
+ * <p>
+ * Note that span offsets always start with 0 and are not affected by RTL.
+ *
+ * @param position The position of the item
+ * @param spanCount The total number of spans in the grid
+ * @return The final span position of the item. Should be between 0 (inclusive) and
+ * <code>spanCount</code>(exclusive)
+ */
+ public int getSpanIndex(int position, int spanCount) {
+ int positionSpanSize = getSpanSize(position);
+ if (positionSpanSize == spanCount) {
+ return 0; // quick return for full-span items
+ }
+ int span = 0;
+ int startPos = 0;
+ // If caching is enabled, try to jump
+ if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
+ int prevKey = findReferenceIndexFromCache(position);
+ if (prevKey >= 0) {
+ span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
+ startPos = prevKey + 1;
+ }
+ }
+ for (int i = startPos; i < position; i++) {
+ int size = getSpanSize(i);
+ span += size;
+ if (span == spanCount) {
+ span = 0;
+ } else if (span > spanCount) {
+ // did not fit, moving to next row / column
+ span = size;
+ }
+ }
+ if (span + positionSpanSize <= spanCount) {
+ return span;
+ }
+ return 0;
+ }
+
+ int findReferenceIndexFromCache(int position) {
+ int lo = 0;
+ int hi = mSpanIndexCache.size() - 1;
+ while (lo <= hi) {
+ final int mid = (lo + hi) >>> 1;
+ final int midVal = mSpanIndexCache.keyAt(mid);
+ if (midVal < position) {
+ lo = mid + 1;
+ } else {
+ hi = mid - 1;
+ }
+ }
+ int index = lo - 1;
+ if (index >= 0 && index < mSpanIndexCache.size()) {
+ return mSpanIndexCache.keyAt(index);
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the index of the group this position belongs.
+ * <p>
+ * For example, if grid has 3 columns and each item occupies 1 span, span group index
+ * for item 1 will be 0, item 5 will be 1.
+ *
+ * @param adapterPosition The position in adapter
+ * @param spanCount The total number of spans in the grid
+ * @return The index of the span group including the item at the given adapter position
+ */
+ public int getSpanGroupIndex(int adapterPosition, int spanCount) {
+ int span = 0;
+ int group = 0;
+ int positionSpanSize = getSpanSize(adapterPosition);
+ for (int i = 0; i < adapterPosition; i++) {
+ int size = getSpanSize(i);
+ span += size;
+ if (span == spanCount) {
+ span = 0;
+ group++;
+ } else if (span > spanCount) {
+ // did not fit, moving to next row / column
+ span = size;
+ group++;
+ }
+ }
+ if (span + positionSpanSize > spanCount) {
+ group++;
+ }
+ return group;
+ }
+ }
+
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return mPendingSavedState == null && !mPendingSpanCountChange;
+ }
+
+ /**
+ * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
+ */
+ public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
+ @Override
+ public int getSpanSize(int position) {
+ return 1;
+ }
+
+ @Override
+ public int getSpanIndex(int position, int spanCount) {
+ return position % spanCount;
+ }
+ }
+
+ /**
+ * LayoutParams used by GridLayoutManager.
+ * <p>
+ * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
+ * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
+ * expected to fill all of the space given to it.
+ */
+ public static class LayoutParams extends RecyclerView.LayoutParams {
+ /**
+ * Span Id for Views that are not laid out yet.
+ */
+ public static final int INVALID_SPAN_ID = -1;
+ int mSpanIndex = INVALID_SPAN_ID;
+ int mSpanSize = 0;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(RecyclerView.LayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * Returns the current span index of this View. If the View is not laid out yet, the return
+ * value is <code>undefined</code>.
+ * <p>
+ * Starting with RecyclerView <b>24.2.0</b>, span indices are always indexed from position 0
+ * even if the layout is RTL. In a vertical GridLayoutManager, <b>leftmost</b> span is span
+ * 0 if the layout is <b>LTR</b> and <b>rightmost</b> span is span 0 if the layout is
+ * <b>RTL</b>. Prior to 24.2.0, it was the opposite which was conflicting with
+ * {@link SpanSizeLookup#getSpanIndex(int, int)}.
+ * <p>
+ * If the View occupies multiple spans, span with the minimum index is returned.
+ *
+ * @return The span index of the View.
+ */
+ public int getSpanIndex() {
+ return mSpanIndex;
+ }
+
+ /**
+ * Returns the number of spans occupied by this View. If the View not laid out yet, the
+ * return value is <code>undefined</code>.
+ *
+ * @return The number of spans occupied by this View.
+ */
+ public int getSpanSize() {
+ return mSpanSize;
+ }
+ }
+}
diff --git a/com/android/internal/widget/IMessagingLayout.java b/com/android/internal/widget/IMessagingLayout.java
new file mode 100644
index 0000000..b72c081
--- /dev/null
+++ b/com/android/internal/widget/IMessagingLayout.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 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.internal.widget;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+
+/**
+ * An interface for a MessagingLayout
+ */
+public interface IMessagingLayout {
+
+ /**
+ * @return the layout containing the messages
+ */
+ MessagingLinearLayout getMessagingLinearLayout();
+
+ /**
+ * @return the context of this view
+ */
+ Context getContext();
+
+ /**
+ * @return the list of messaging groups
+ */
+ ArrayList<MessagingGroup> getMessagingGroups();
+
+ /**
+ * Disable the clipping of the messaging container.
+ */
+ void setMessagingClippingDisabled(boolean clippingDisabled);
+}
diff --git a/com/android/internal/widget/ImageFloatingTextView.java b/com/android/internal/widget/ImageFloatingTextView.java
new file mode 100644
index 0000000..e143498
--- /dev/null
+++ b/com/android/internal/widget/ImageFloatingTextView.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.text.BoringLayout;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextUtils;
+import android.text.method.TransformationMethod;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+/**
+ * A TextView that can float around an image on the end.
+ *
+ * @hide
+ */
[email protected]
+public class ImageFloatingTextView extends TextView {
+
+ /** Number of lines from the top to indent */
+ private int mIndentLines;
+
+ /** Resolved layout direction */
+ private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED;
+ private int mMaxLinesForHeight = -1;
+ private int mLayoutMaxLines = -1;
+ private int mImageEndMargin;
+
+ public ImageFloatingTextView(Context context) {
+ this(context, null);
+ }
+
+ public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
+ Layout.Alignment alignment, boolean shouldEllipsize,
+ TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
+ TransformationMethod transformationMethod = getTransformationMethod();
+ CharSequence text = getText();
+ if (transformationMethod != null) {
+ text = transformationMethod.getTransformation(text, this);
+ }
+ text = text == null ? "" : text;
+ StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(),
+ getPaint(), wantWidth)
+ .setAlignment(alignment)
+ .setTextDirection(getTextDirectionHeuristic())
+ .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
+ .setIncludePad(getIncludeFontPadding())
+ .setUseLineSpacingFromFallbacks(true)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL);
+ int maxLines;
+ if (mMaxLinesForHeight > 0) {
+ maxLines = mMaxLinesForHeight;
+ } else {
+ maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
+ }
+ builder.setMaxLines(maxLines);
+ mLayoutMaxLines = maxLines;
+ if (shouldEllipsize) {
+ builder.setEllipsize(effectiveEllipsize)
+ .setEllipsizedWidth(ellipsisWidth);
+ }
+
+ // we set the endmargin on the requested number of lines.
+ int[] margins = null;
+ if (mIndentLines > 0) {
+ margins = new int[mIndentLines + 1];
+ for (int i = 0; i < mIndentLines; i++) {
+ margins[i] = mImageEndMargin;
+ }
+ }
+ if (mResolvedDirection == LAYOUT_DIRECTION_RTL) {
+ builder.setIndents(margins, null);
+ } else {
+ builder.setIndents(null, margins);
+ }
+
+ return builder.build();
+ }
+
+ @RemotableViewMethod
+ public void setImageEndMargin(int imageEndMargin) {
+ mImageEndMargin = imageEndMargin;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom;
+ if (getLayout() != null && getLayout().getHeight() != availableHeight) {
+ // We've been measured before and the new size is different than before, lets make sure
+ // we reset the maximum lines, otherwise we may be cut short
+ mMaxLinesForHeight = -1;
+ nullLayouts();
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ Layout layout = getLayout();
+ if (layout.getHeight() > availableHeight) {
+ // With the existing layout, not all of our lines fit on the screen, let's find the
+ // first one that fits and ellipsize at that one.
+ int maxLines = layout.getLineCount() - 1;
+ while (maxLines > 1 && layout.getLineBottom(maxLines - 1) > availableHeight) {
+ maxLines--;
+ }
+ if (getMaxLines() > 0) {
+ maxLines = Math.min(getMaxLines(), maxLines);
+ }
+ // Only if the number of lines is different from the current layout, we recreate it.
+ if (maxLines != mLayoutMaxLines) {
+ mMaxLinesForHeight = maxLines;
+ nullLayouts();
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) {
+ mResolvedDirection = layoutDirection;
+ if (mIndentLines > 0) {
+ // Invalidate layout.
+ nullLayouts();
+ requestLayout();
+ }
+ }
+ }
+
+ @RemotableViewMethod
+ public void setHasImage(boolean hasImage) {
+ setNumIndentLines(hasImage ? 2 : 0);
+ }
+
+ /**
+ * @param lines the number of lines at the top that should be indented by indentEnd
+ * @return whether a change was made
+ */
+ public boolean setNumIndentLines(int lines) {
+ if (mIndentLines != lines) {
+ mIndentLines = lines;
+ // Invalidate layout.
+ nullLayouts();
+ requestLayout();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/com/android/internal/widget/ImageMessageConsumer.java b/com/android/internal/widget/ImageMessageConsumer.java
new file mode 100644
index 0000000..01613dc
--- /dev/null
+++ b/com/android/internal/widget/ImageMessageConsumer.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+/**
+ * An interface for the class who will use the {@link ImageResolver} to resolve images.
+ */
+public interface ImageMessageConsumer {
+ /**
+ * Set the custom {@link ImageResolver} other than {@link LocalImageResolver}.
+ * @param resolver An image resolver that has custom implementation.
+ */
+ void setImageResolver(ImageResolver resolver);
+}
diff --git a/com/android/internal/widget/ImageResolver.java b/com/android/internal/widget/ImageResolver.java
new file mode 100644
index 0000000..4588525
--- /dev/null
+++ b/com/android/internal/widget/ImageResolver.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+/**
+ * An interface for image resolvers that have custom implementations like cache mechanisms.
+ */
+public interface ImageResolver {
+ /**
+ * Load an image from specified uri.
+ * @param uri Uri of the target image.
+ * @return Target image in Drawable.
+ */
+ Drawable loadImage(Uri uri);
+}
diff --git a/com/android/internal/widget/InlinePresentationStyleUtils.java b/com/android/internal/widget/InlinePresentationStyleUtils.java
new file mode 100644
index 0000000..9c1aa28
--- /dev/null
+++ b/com/android/internal/widget/InlinePresentationStyleUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 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.internal.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility methods relating to inline presentation UI.
+ */
+public final class InlinePresentationStyleUtils {
+
+ /**
+ * Returns true if the two bundles are deeply equal.
+ *
+ * Each input bundle may represent a UI style in the
+ * {@link android.widget.inline.InlinePresentationSpec} or the extra
+ * request info in the {@link android.view.inputmethod.InlineSuggestionsRequest}
+ *
+ * Note: this method should not be called in the framework process for security reasons.
+ */
+ public static boolean bundleEquals(@NonNull Bundle bundle1, @NonNull Bundle bundle2) {
+ if (bundle1 == bundle2) {
+ return true;
+ }
+ if (bundle1 == null || bundle2 == null) {
+ return false;
+ }
+ if (bundle1.size() != bundle2.size()) {
+ return false;
+ }
+ Set<String> keys = bundle1.keySet();
+ for (String key : keys) {
+ Object value1 = bundle1.get(key);
+ Object value2 = bundle2.get(key);
+ final boolean equal = value1 instanceof Bundle && value2 instanceof Bundle
+ ? bundleEquals((Bundle) value1, (Bundle) value2)
+ : Objects.equals(value1, value2);
+ if (!equal) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Removes remote objects from the bundle.
+ */
+ public static void filterContentTypes(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return;
+ }
+
+ for (String key : bundle.keySet()) {
+ Object o = bundle.get(key);
+
+ if (o instanceof Bundle) {
+ filterContentTypes((Bundle) o);
+ } else if (o instanceof IBinder) {
+ bundle.remove(key);
+ }
+ }
+ }
+
+ /**
+ * Private ctor to avoid constructing the class.
+ */
+ private InlinePresentationStyleUtils() {
+ }
+}
diff --git a/com/android/internal/widget/LinearLayoutManager.java b/com/android/internal/widget/LinearLayoutManager.java
new file mode 100644
index 0000000..0000a74
--- /dev/null
+++ b/com/android/internal/widget/LinearLayoutManager.java
@@ -0,0 +1,2394 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import static com.android.internal.widget.RecyclerView.NO_POSITION;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.widget.RecyclerView.LayoutParams;
+import com.android.internal.widget.helper.ItemTouchHelper;
+
+import java.util.List;
+
+/**
+ * A {@link com.android.internal.widget.RecyclerView.LayoutManager} implementation which provides
+ * similar functionality to {@link android.widget.ListView}.
+ */
+public class LinearLayoutManager extends RecyclerView.LayoutManager implements
+ ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
+
+ private static final String TAG = "LinearLayoutManager";
+
+ static final boolean DEBUG = false;
+
+ public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
+
+ public static final int VERTICAL = OrientationHelper.VERTICAL;
+
+ public static final int INVALID_OFFSET = Integer.MIN_VALUE;
+
+
+ /**
+ * While trying to find next view to focus, LayoutManager will not try to scroll more
+ * than this factor times the total space of the list. If layout is vertical, total space is the
+ * height minus padding, if layout is horizontal, total space is the width minus padding.
+ */
+ private static final float MAX_SCROLL_FACTOR = 1 / 3f;
+
+
+ /**
+ * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ int mOrientation;
+
+ /**
+ * Helper class that keeps temporary layout state.
+ * It does not keep state after layout is complete but we still keep a reference to re-use
+ * the same object.
+ */
+ private LayoutState mLayoutState;
+
+ /**
+ * Many calculations are made depending on orientation. To keep it clean, this interface
+ * helps {@link LinearLayoutManager} make those decisions.
+ * Based on {@link #mOrientation}, an implementation is lazily created in
+ * {@link #ensureLayoutState} method.
+ */
+ OrientationHelper mOrientationHelper;
+
+ /**
+ * We need to track this so that we can ignore current position when it changes.
+ */
+ private boolean mLastStackFromEnd;
+
+
+ /**
+ * Defines if layout should be calculated from end to start.
+ *
+ * @see #mShouldReverseLayout
+ */
+ private boolean mReverseLayout = false;
+
+ /**
+ * This keeps the final value for how LayoutManager should start laying out views.
+ * It is calculated by checking {@link #getReverseLayout()} and View's layout direction.
+ * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run.
+ */
+ boolean mShouldReverseLayout = false;
+
+ /**
+ * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and
+ * it supports both orientations.
+ * see {@link android.widget.AbsListView#setStackFromBottom(boolean)}
+ */
+ private boolean mStackFromEnd = false;
+
+ /**
+ * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}.
+ * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}
+ */
+ private boolean mSmoothScrollbarEnabled = true;
+
+ /**
+ * When LayoutManager needs to scroll to a position, it sets this variable and requests a
+ * layout which will check this variable and re-layout accordingly.
+ */
+ int mPendingScrollPosition = NO_POSITION;
+
+ /**
+ * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
+ * called.
+ */
+ int mPendingScrollPositionOffset = INVALID_OFFSET;
+
+ private boolean mRecycleChildrenOnDetach;
+
+ SavedState mPendingSavedState = null;
+
+ /**
+ * Re-used variable to keep anchor information on re-layout.
+ * Anchor position and coordinate defines the reference point for LLM while doing a layout.
+ * */
+ final AnchorInfo mAnchorInfo = new AnchorInfo();
+
+ /**
+ * Stashed to avoid allocation, currently only used in #fill()
+ */
+ private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult();
+
+ /**
+ * Number of items to prefetch when first coming on screen with new data.
+ */
+ private int mInitialItemPrefetchCount = 2;
+
+ /**
+ * Creates a vertical LinearLayoutManager
+ *
+ * @param context Current context, will be used to access resources.
+ */
+ public LinearLayoutManager(Context context) {
+ this(context, VERTICAL, false);
+ }
+
+ /**
+ * @param context Current context, will be used to access resources.
+ * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
+ * #VERTICAL}.
+ * @param reverseLayout When set to true, layouts from end to start.
+ */
+ public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
+ setOrientation(orientation);
+ setReverseLayout(reverseLayout);
+ setAutoMeasureEnabled(true);
+ }
+
+ /**
+ * Constructor used when layout manager is set in XML by RecyclerView attribute
+ * "layoutManager". Defaults to vertical orientation.
+ */
+ public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
+ setOrientation(properties.orientation);
+ setReverseLayout(properties.reverseLayout);
+ setStackFromEnd(properties.stackFromEnd);
+ setAutoMeasureEnabled(true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ /**
+ * Returns whether LayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ *
+ * @return true if LayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ */
+ public boolean getRecycleChildrenOnDetach() {
+ return mRecycleChildrenOnDetach;
+ }
+
+ /**
+ * Set whether LayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ * <p>
+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
+ * this flag to <code>true</code> so that views will be available to other RecyclerViews
+ * immediately.
+ * <p>
+ * Note that, setting this flag will result in a performance drop if RecyclerView
+ * is restored.
+ *
+ * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
+ */
+ public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
+ mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+ }
+
+ @Override
+ public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
+ super.onDetachedFromWindow(view, recycler);
+ if (mRecycleChildrenOnDetach) {
+ removeAndRecycleAllViews(recycler);
+ recycler.clear();
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ if (getChildCount() > 0) {
+ event.setFromIndex(findFirstVisibleItemPosition());
+ event.setToIndex(findLastVisibleItemPosition());
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mPendingSavedState != null) {
+ return new SavedState(mPendingSavedState);
+ }
+ SavedState state = new SavedState();
+ if (getChildCount() > 0) {
+ ensureLayoutState();
+ boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
+ state.mAnchorLayoutFromEnd = didLayoutFromEnd;
+ if (didLayoutFromEnd) {
+ final View refChild = getChildClosestToEnd();
+ state.mAnchorOffset = mOrientationHelper.getEndAfterPadding()
+ - mOrientationHelper.getDecoratedEnd(refChild);
+ state.mAnchorPosition = getPosition(refChild);
+ } else {
+ final View refChild = getChildClosestToStart();
+ state.mAnchorPosition = getPosition(refChild);
+ state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild)
+ - mOrientationHelper.getStartAfterPadding();
+ }
+ } else {
+ state.invalidateAnchor();
+ }
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ mPendingSavedState = (SavedState) state;
+ requestLayout();
+ if (DEBUG) {
+ Log.d(TAG, "loaded saved state");
+ }
+ } else if (DEBUG) {
+ Log.d(TAG, "invalid saved state class");
+ }
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
+ */
+ @Override
+ public boolean canScrollHorizontally() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #VERTICAL}
+ */
+ @Override
+ public boolean canScrollVertically() {
+ return mOrientation == VERTICAL;
+ }
+
+ /**
+ * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)}
+ */
+ public void setStackFromEnd(boolean stackFromEnd) {
+ assertNotInLayoutOrScroll(null);
+ if (mStackFromEnd == stackFromEnd) {
+ return;
+ }
+ mStackFromEnd = stackFromEnd;
+ requestLayout();
+ }
+
+ public boolean getStackFromEnd() {
+ return mStackFromEnd;
+ }
+
+ /**
+ * Returns the current orientation of the layout.
+ *
+ * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @see #setOrientation(int)
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ * Sets the orientation of the layout. {@link com.android.internal.widget.LinearLayoutManager}
+ * will do its best to keep scroll position.
+ *
+ * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ throw new IllegalArgumentException("invalid orientation:" + orientation);
+ }
+ assertNotInLayoutOrScroll(null);
+ if (orientation == mOrientation) {
+ return;
+ }
+ mOrientation = orientation;
+ mOrientationHelper = null;
+ requestLayout();
+ }
+
+ /**
+ * Calculates the view layout order. (e.g. from end to start or start to end)
+ * RTL layout support is applied automatically. So if layout is RTL and
+ * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
+ */
+ private void resolveShouldLayoutReverse() {
+ // A == B is the same result, but we rather keep it readable
+ if (mOrientation == VERTICAL || !isLayoutRTL()) {
+ mShouldReverseLayout = mReverseLayout;
+ } else {
+ mShouldReverseLayout = !mReverseLayout;
+ }
+ }
+
+ /**
+ * Returns if views are laid out from the opposite direction of the layout.
+ *
+ * @return If layout is reversed or not.
+ * @see #setReverseLayout(boolean)
+ */
+ public boolean getReverseLayout() {
+ return mReverseLayout;
+ }
+
+ /**
+ * Used to reverse item traversal and layout order.
+ * This behaves similar to the layout change for RTL views. When set to true, first item is
+ * laid out at the end of the UI, second item is laid out before it etc.
+ *
+ * For horizontal layouts, it depends on the layout direction.
+ * When set to true, If {@link com.android.internal.widget.RecyclerView} is LTR, than it will
+ * layout from RTL, if {@link com.android.internal.widget.RecyclerView}} is RTL, it will layout
+ * from LTR.
+ *
+ * If you are looking for the exact same behavior of
+ * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use
+ * {@link #setStackFromEnd(boolean)}
+ */
+ public void setReverseLayout(boolean reverseLayout) {
+ assertNotInLayoutOrScroll(null);
+ if (reverseLayout == mReverseLayout) {
+ return;
+ }
+ mReverseLayout = reverseLayout;
+ requestLayout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View findViewByPosition(int position) {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return null;
+ }
+ final int firstChild = getPosition(getChildAt(0));
+ final int viewPosition = position - firstChild;
+ if (viewPosition >= 0 && viewPosition < childCount) {
+ final View child = getChildAt(viewPosition);
+ if (getPosition(child) == position) {
+ return child; // in pre-layout, this may not match
+ }
+ }
+ // fallback to traversal. This might be necessary in pre-layout.
+ return super.findViewByPosition(position);
+ }
+
+ /**
+ * <p>Returns the amount of extra space that should be laid out by LayoutManager.</p>
+ *
+ * <p>By default, {@link com.android.internal.widget.LinearLayoutManager} lays out 1 extra page
+ * of items while smooth scrolling and 0 otherwise. You can override this method to implement
+ * your custom layout pre-cache logic.</p>
+ *
+ * <p><strong>Note:</strong>Laying out invisible elements generally comes with significant
+ * performance cost. It's typically only desirable in places like smooth scrolling to an unknown
+ * location, where 1) the extra content helps LinearLayoutManager know in advance when its
+ * target is approaching, so it can decelerate early and smoothly and 2) while motion is
+ * continuous.</p>
+ *
+ * <p>Extending the extra layout space is especially expensive if done while the user may change
+ * scrolling direction. Changing direction will cause the extra layout space to swap to the
+ * opposite side of the viewport, incurring many rebinds/recycles, unless the cache is large
+ * enough to handle it.</p>
+ *
+ * @return The extra space that should be laid out (in pixels).
+ */
+ protected int getExtraLayoutSpace(RecyclerView.State state) {
+ if (state.hasTargetScrollPosition()) {
+ return mOrientationHelper.getTotalSpace();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+ int position) {
+ LinearSmoothScroller linearSmoothScroller =
+ new LinearSmoothScroller(recyclerView.getContext());
+ linearSmoothScroller.setTargetPosition(position);
+ startSmoothScroll(linearSmoothScroller);
+ }
+
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ if (getChildCount() == 0) {
+ return null;
+ }
+ final int firstChildPos = getPosition(getChildAt(0));
+ final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
+ if (mOrientation == HORIZONTAL) {
+ return new PointF(direction, 0);
+ } else {
+ return new PointF(0, direction);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ // layout algorithm:
+ // 1) by checking children and other variables, find an anchor coordinate and an anchor
+ // item position.
+ // 2) fill towards start, stacking from bottom
+ // 3) fill towards end, stacking from top
+ // 4) scroll to fulfill requirements like stack from bottom.
+ // create layout state
+ if (DEBUG) {
+ Log.d(TAG, "is pre layout:" + state.isPreLayout());
+ }
+ if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
+ if (state.getItemCount() == 0) {
+ removeAndRecycleAllViews(recycler);
+ return;
+ }
+ }
+ if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
+ mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
+ }
+
+ ensureLayoutState();
+ mLayoutState.mRecycle = false;
+ // resolve layout direction
+ resolveShouldLayoutReverse();
+
+ if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
+ || mPendingSavedState != null) {
+ mAnchorInfo.reset();
+ mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
+ // calculate anchor position and coordinate
+ updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
+ mAnchorInfo.mValid = true;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Anchor info:" + mAnchorInfo);
+ }
+
+ // LLM may decide to layout items for "extra" pixels to account for scrolling target,
+ // caching or predictive animations.
+ int extraForStart;
+ int extraForEnd;
+ final int extra = getExtraLayoutSpace(state);
+ // If the previous scroll delta was less than zero, the extra space should be laid out
+ // at the start. Otherwise, it should be at the end.
+ if (mLayoutState.mLastScrollDelta >= 0) {
+ extraForEnd = extra;
+ extraForStart = 0;
+ } else {
+ extraForStart = extra;
+ extraForEnd = 0;
+ }
+ extraForStart += mOrientationHelper.getStartAfterPadding();
+ extraForEnd += mOrientationHelper.getEndPadding();
+ if (state.isPreLayout() && mPendingScrollPosition != NO_POSITION
+ && mPendingScrollPositionOffset != INVALID_OFFSET) {
+ // if the child is visible and we are going to move it around, we should layout
+ // extra items in the opposite direction to make sure new items animate nicely
+ // instead of just fading in
+ final View existing = findViewByPosition(mPendingScrollPosition);
+ if (existing != null) {
+ final int current;
+ final int upcomingOffset;
+ if (mShouldReverseLayout) {
+ current = mOrientationHelper.getEndAfterPadding()
+ - mOrientationHelper.getDecoratedEnd(existing);
+ upcomingOffset = current - mPendingScrollPositionOffset;
+ } else {
+ current = mOrientationHelper.getDecoratedStart(existing)
+ - mOrientationHelper.getStartAfterPadding();
+ upcomingOffset = mPendingScrollPositionOffset - current;
+ }
+ if (upcomingOffset > 0) {
+ extraForStart += upcomingOffset;
+ } else {
+ extraForEnd -= upcomingOffset;
+ }
+ }
+ }
+ int startOffset;
+ int endOffset;
+ final int firstLayoutDirection;
+ if (mAnchorInfo.mLayoutFromEnd) {
+ firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
+ : LayoutState.ITEM_DIRECTION_HEAD;
+ } else {
+ firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
+ : LayoutState.ITEM_DIRECTION_TAIL;
+ }
+
+ onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
+ detachAndScrapAttachedViews(recycler);
+ mLayoutState.mInfinite = resolveIsInfinite();
+ mLayoutState.mIsPreLayout = state.isPreLayout();
+ if (mAnchorInfo.mLayoutFromEnd) {
+ // fill towards start
+ updateLayoutStateToFillStart(mAnchorInfo);
+ mLayoutState.mExtra = extraForStart;
+ fill(recycler, mLayoutState, state, false);
+ startOffset = mLayoutState.mOffset;
+ final int firstElement = mLayoutState.mCurrentPosition;
+ if (mLayoutState.mAvailable > 0) {
+ extraForEnd += mLayoutState.mAvailable;
+ }
+ // fill towards end
+ updateLayoutStateToFillEnd(mAnchorInfo);
+ mLayoutState.mExtra = extraForEnd;
+ mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+ fill(recycler, mLayoutState, state, false);
+ endOffset = mLayoutState.mOffset;
+
+ if (mLayoutState.mAvailable > 0) {
+ // end could not consume all. add more items towards start
+ extraForStart = mLayoutState.mAvailable;
+ updateLayoutStateToFillStart(firstElement, startOffset);
+ mLayoutState.mExtra = extraForStart;
+ fill(recycler, mLayoutState, state, false);
+ startOffset = mLayoutState.mOffset;
+ }
+ } else {
+ // fill towards end
+ updateLayoutStateToFillEnd(mAnchorInfo);
+ mLayoutState.mExtra = extraForEnd;
+ fill(recycler, mLayoutState, state, false);
+ endOffset = mLayoutState.mOffset;
+ final int lastElement = mLayoutState.mCurrentPosition;
+ if (mLayoutState.mAvailable > 0) {
+ extraForStart += mLayoutState.mAvailable;
+ }
+ // fill towards start
+ updateLayoutStateToFillStart(mAnchorInfo);
+ mLayoutState.mExtra = extraForStart;
+ mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+ fill(recycler, mLayoutState, state, false);
+ startOffset = mLayoutState.mOffset;
+
+ if (mLayoutState.mAvailable > 0) {
+ extraForEnd = mLayoutState.mAvailable;
+ // start could not consume all it should. add more items towards end
+ updateLayoutStateToFillEnd(lastElement, endOffset);
+ mLayoutState.mExtra = extraForEnd;
+ fill(recycler, mLayoutState, state, false);
+ endOffset = mLayoutState.mOffset;
+ }
+ }
+
+ // changes may cause gaps on the UI, try to fix them.
+ // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
+ // changed
+ if (getChildCount() > 0) {
+ // because layout from end may be changed by scroll to position
+ // we re-calculate it.
+ // find which side we should check for gaps.
+ if (mShouldReverseLayout ^ mStackFromEnd) {
+ int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
+ fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
+ } else {
+ int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
+ fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
+ startOffset += fixOffset;
+ endOffset += fixOffset;
+ }
+ }
+ layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
+ if (!state.isPreLayout()) {
+ mOrientationHelper.onLayoutComplete();
+ } else {
+ mAnchorInfo.reset();
+ }
+ mLastStackFromEnd = mStackFromEnd;
+ if (DEBUG) {
+ validateChildOrder();
+ }
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ super.onLayoutCompleted(state);
+ mPendingSavedState = null; // we don't need this anymore
+ mPendingScrollPosition = NO_POSITION;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ mAnchorInfo.reset();
+ }
+
+ /**
+ * Method called when Anchor position is decided. Extending class can setup accordingly or
+ * even update anchor info if necessary.
+ * @param recycler The recycler for the layout
+ * @param state The layout state
+ * @param anchorInfo The mutable POJO that keeps the position and offset.
+ * @param firstLayoutItemDirection The direction of the first layout filling in terms of adapter
+ * indices.
+ */
+ void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state,
+ AnchorInfo anchorInfo, int firstLayoutItemDirection) {
+ }
+
+ /**
+ * If necessary, layouts new items for predictive animations
+ */
+ private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,
+ RecyclerView.State state, int startOffset, int endOffset) {
+ // If there are scrap children that we did not layout, we need to find where they did go
+ // and layout them accordingly so that animations can work as expected.
+ // This case may happen if new views are added or an existing view expands and pushes
+ // another view out of bounds.
+ if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout()
+ || !supportsPredictiveItemAnimations()) {
+ return;
+ }
+ // to make the logic simpler, we calculate the size of children and call fill.
+ int scrapExtraStart = 0, scrapExtraEnd = 0;
+ final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
+ final int scrapSize = scrapList.size();
+ final int firstChildPos = getPosition(getChildAt(0));
+ for (int i = 0; i < scrapSize; i++) {
+ RecyclerView.ViewHolder scrap = scrapList.get(i);
+ if (scrap.isRemoved()) {
+ continue;
+ }
+ final int position = scrap.getLayoutPosition();
+ final int direction = position < firstChildPos != mShouldReverseLayout
+ ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
+ if (direction == LayoutState.LAYOUT_START) {
+ scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+ } else {
+ scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart
+ + " towards start and " + scrapExtraEnd + " towards end");
+ }
+ mLayoutState.mScrapList = scrapList;
+ if (scrapExtraStart > 0) {
+ View anchor = getChildClosestToStart();
+ updateLayoutStateToFillStart(getPosition(anchor), startOffset);
+ mLayoutState.mExtra = scrapExtraStart;
+ mLayoutState.mAvailable = 0;
+ mLayoutState.assignPositionFromScrapList();
+ fill(recycler, mLayoutState, state, false);
+ }
+
+ if (scrapExtraEnd > 0) {
+ View anchor = getChildClosestToEnd();
+ updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
+ mLayoutState.mExtra = scrapExtraEnd;
+ mLayoutState.mAvailable = 0;
+ mLayoutState.assignPositionFromScrapList();
+ fill(recycler, mLayoutState, state, false);
+ }
+ mLayoutState.mScrapList = null;
+ }
+
+ private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
+ AnchorInfo anchorInfo) {
+ if (updateAnchorFromPendingData(state, anchorInfo)) {
+ if (DEBUG) {
+ Log.d(TAG, "updated anchor info from pending information");
+ }
+ return;
+ }
+
+ if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
+ if (DEBUG) {
+ Log.d(TAG, "updated anchor info from existing children");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "deciding anchor info for fresh state");
+ }
+ anchorInfo.assignCoordinateFromPadding();
+ anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
+ }
+
+ /**
+ * Finds an anchor child from existing Views. Most of the time, this is the view closest to
+ * start or end that has a valid position (e.g. not removed).
+ * <p>
+ * If a child has focus, it is given priority.
+ */
+ private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
+ RecyclerView.State state, AnchorInfo anchorInfo) {
+ if (getChildCount() == 0) {
+ return false;
+ }
+ final View focused = getFocusedChild();
+ if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
+ anchorInfo.assignFromViewAndKeepVisibleRect(focused);
+ return true;
+ }
+ if (mLastStackFromEnd != mStackFromEnd) {
+ return false;
+ }
+ View referenceChild = anchorInfo.mLayoutFromEnd
+ ? findReferenceChildClosestToEnd(recycler, state)
+ : findReferenceChildClosestToStart(recycler, state);
+ if (referenceChild != null) {
+ anchorInfo.assignFromView(referenceChild);
+ // If all visible views are removed in 1 pass, reference child might be out of bounds.
+ // If that is the case, offset it back to 0 so that we use these pre-layout children.
+ if (!state.isPreLayout() && supportsPredictiveItemAnimations()) {
+ // validate this child is at least partially visible. if not, offset it to start
+ final boolean notVisible =
+ mOrientationHelper.getDecoratedStart(referenceChild) >= mOrientationHelper
+ .getEndAfterPadding()
+ || mOrientationHelper.getDecoratedEnd(referenceChild)
+ < mOrientationHelper.getStartAfterPadding();
+ if (notVisible) {
+ anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd
+ ? mOrientationHelper.getEndAfterPadding()
+ : mOrientationHelper.getStartAfterPadding();
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If there is a pending scroll position or saved states, updates the anchor info from that
+ * data and returns true
+ */
+ private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
+ if (state.isPreLayout() || mPendingScrollPosition == NO_POSITION) {
+ return false;
+ }
+ // validate scroll position
+ if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
+ mPendingScrollPosition = NO_POSITION;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ if (DEBUG) {
+ Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition);
+ }
+ return false;
+ }
+
+ // if child is visible, try to make it a reference child and ensure it is fully visible.
+ // if child is not visible, align it depending on its virtual position.
+ anchorInfo.mPosition = mPendingScrollPosition;
+ if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
+ // Anchor offset depends on how that child was laid out. Here, we update it
+ // according to our current view bounds
+ anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+ if (anchorInfo.mLayoutFromEnd) {
+ anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
+ - mPendingSavedState.mAnchorOffset;
+ } else {
+ anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding()
+ + mPendingSavedState.mAnchorOffset;
+ }
+ return true;
+ }
+
+ if (mPendingScrollPositionOffset == INVALID_OFFSET) {
+ View child = findViewByPosition(mPendingScrollPosition);
+ if (child != null) {
+ final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
+ if (childSize > mOrientationHelper.getTotalSpace()) {
+ // item does not fit. fix depending on layout direction
+ anchorInfo.assignCoordinateFromPadding();
+ return true;
+ }
+ final int startGap = mOrientationHelper.getDecoratedStart(child)
+ - mOrientationHelper.getStartAfterPadding();
+ if (startGap < 0) {
+ anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding();
+ anchorInfo.mLayoutFromEnd = false;
+ return true;
+ }
+ final int endGap = mOrientationHelper.getEndAfterPadding()
+ - mOrientationHelper.getDecoratedEnd(child);
+ if (endGap < 0) {
+ anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding();
+ anchorInfo.mLayoutFromEnd = true;
+ return true;
+ }
+ anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd
+ ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper
+ .getTotalSpaceChange())
+ : mOrientationHelper.getDecoratedStart(child);
+ } else { // item is not visible.
+ if (getChildCount() > 0) {
+ // get position of any child, does not matter
+ int pos = getPosition(getChildAt(0));
+ anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos
+ == mShouldReverseLayout;
+ }
+ anchorInfo.assignCoordinateFromPadding();
+ }
+ return true;
+ }
+ // override layout from end values for consistency
+ anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
+ // if this changes, we should update prepareForDrop as well
+ if (mShouldReverseLayout) {
+ anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
+ - mPendingScrollPositionOffset;
+ } else {
+ anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding()
+ + mPendingScrollPositionOffset;
+ }
+ return true;
+ }
+
+ /**
+ * @return The final offset amount for children
+ */
+ private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler,
+ RecyclerView.State state, boolean canOffsetChildren) {
+ int gap = mOrientationHelper.getEndAfterPadding() - endOffset;
+ int fixOffset = 0;
+ if (gap > 0) {
+ fixOffset = -scrollBy(-gap, recycler, state);
+ } else {
+ return 0; // nothing to fix
+ }
+ // move offset according to scroll amount
+ endOffset += fixOffset;
+ if (canOffsetChildren) {
+ // re-calculate gap, see if we could fix it
+ gap = mOrientationHelper.getEndAfterPadding() - endOffset;
+ if (gap > 0) {
+ mOrientationHelper.offsetChildren(gap);
+ return gap + fixOffset;
+ }
+ }
+ return fixOffset;
+ }
+
+ /**
+ * @return The final offset amount for children
+ */
+ private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler,
+ RecyclerView.State state, boolean canOffsetChildren) {
+ int gap = startOffset - mOrientationHelper.getStartAfterPadding();
+ int fixOffset = 0;
+ if (gap > 0) {
+ // check if we should fix this gap.
+ fixOffset = -scrollBy(gap, recycler, state);
+ } else {
+ return 0; // nothing to fix
+ }
+ startOffset += fixOffset;
+ if (canOffsetChildren) {
+ // re-calculate gap, see if we could fix it
+ gap = startOffset - mOrientationHelper.getStartAfterPadding();
+ if (gap > 0) {
+ mOrientationHelper.offsetChildren(-gap);
+ return fixOffset - gap;
+ }
+ }
+ return fixOffset;
+ }
+
+ private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
+ updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
+ }
+
+ private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
+ mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
+ LayoutState.ITEM_DIRECTION_TAIL;
+ mLayoutState.mCurrentPosition = itemPosition;
+ mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
+ mLayoutState.mOffset = offset;
+ mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
+ }
+
+ private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) {
+ updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate);
+ }
+
+ private void updateLayoutStateToFillStart(int itemPosition, int offset) {
+ mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();
+ mLayoutState.mCurrentPosition = itemPosition;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
+ LayoutState.ITEM_DIRECTION_HEAD;
+ mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START;
+ mLayoutState.mOffset = offset;
+ mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
+
+ }
+
+ protected boolean isLayoutRTL() {
+ return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ void ensureLayoutState() {
+ if (mLayoutState == null) {
+ mLayoutState = createLayoutState();
+ }
+ if (mOrientationHelper == null) {
+ mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
+ }
+ }
+
+ /**
+ * Test overrides this to plug some tracking and verification.
+ *
+ * @return A new LayoutState
+ */
+ LayoutState createLayoutState() {
+ return new LayoutState();
+ }
+
+ /**
+ * <p>Scroll the RecyclerView to make the position visible.</p>
+ *
+ * <p>RecyclerView will scroll the minimum amount that is necessary to make the
+ * target position visible. If you are looking for a similar behavior to
+ * {@link android.widget.ListView#setSelection(int)} or
+ * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use
+ * {@link #scrollToPositionWithOffset(int, int)}.</p>
+ *
+ * <p>Note that scroll position change will not be reflected until the next layout call.</p>
+ *
+ * @param position Scroll to this adapter position
+ * @see #scrollToPositionWithOffset(int, int)
+ */
+ @Override
+ public void scrollToPosition(int position) {
+ mPendingScrollPosition = position;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ if (mPendingSavedState != null) {
+ mPendingSavedState.invalidateAnchor();
+ }
+ requestLayout();
+ }
+
+ /**
+ * Scroll to the specified adapter position with the given offset from resolved layout
+ * start. Resolved layout start depends on {@link #getReverseLayout()},
+ * {@link View#getLayoutDirection()} and {@link #getStackFromEnd()}.
+ * <p>
+ * For example, if layout is {@link #VERTICAL} and {@link #getStackFromEnd()} is true, calling
+ * <code>scrollToPositionWithOffset(10, 20)</code> will layout such that
+ * <code>item[10]</code>'s bottom is 20 pixels above the RecyclerView's bottom.
+ * <p>
+ * Note that scroll position change will not be reflected until the next layout call.
+ * <p>
+ * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
+ *
+ * @param position Index (starting at 0) of the reference item.
+ * @param offset The distance (in pixels) between the start edge of the item view and
+ * start edge of the RecyclerView.
+ * @see #setReverseLayout(boolean)
+ * @see #scrollToPosition(int)
+ */
+ public void scrollToPositionWithOffset(int position, int offset) {
+ mPendingScrollPosition = position;
+ mPendingScrollPositionOffset = offset;
+ if (mPendingSavedState != null) {
+ mPendingSavedState.invalidateAnchor();
+ }
+ requestLayout();
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (mOrientation == VERTICAL) {
+ return 0;
+ }
+ return scrollBy(dx, recycler, state);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (mOrientation == HORIZONTAL) {
+ return 0;
+ }
+ return scrollBy(dy, recycler, state);
+ }
+
+ @Override
+ public int computeHorizontalScrollOffset(RecyclerView.State state) {
+ return computeScrollOffset(state);
+ }
+
+ @Override
+ public int computeVerticalScrollOffset(RecyclerView.State state) {
+ return computeScrollOffset(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollExtent(RecyclerView.State state) {
+ return computeScrollExtent(state);
+ }
+
+ @Override
+ public int computeVerticalScrollExtent(RecyclerView.State state) {
+ return computeScrollExtent(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollRange(RecyclerView.State state) {
+ return computeScrollRange(state);
+ }
+
+ @Override
+ public int computeVerticalScrollRange(RecyclerView.State state) {
+ return computeScrollRange(state);
+ }
+
+ private int computeScrollOffset(RecyclerView.State state) {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+ ensureLayoutState();
+ return ScrollbarHelper.computeScrollOffset(state, mOrientationHelper,
+ findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
+ findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
+ this, mSmoothScrollbarEnabled, mShouldReverseLayout);
+ }
+
+ private int computeScrollExtent(RecyclerView.State state) {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+ ensureLayoutState();
+ return ScrollbarHelper.computeScrollExtent(state, mOrientationHelper,
+ findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
+ findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
+ this, mSmoothScrollbarEnabled);
+ }
+
+ private int computeScrollRange(RecyclerView.State state) {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+ ensureLayoutState();
+ return ScrollbarHelper.computeScrollRange(state, mOrientationHelper,
+ findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
+ findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
+ this, mSmoothScrollbarEnabled);
+ }
+
+ /**
+ * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed
+ * based on the number of visible pixels in the visible items. This however assumes that all
+ * list items have similar or equal widths or heights (depending on list orientation).
+ * If you use a list in which items have different dimensions, the scrollbar will change
+ * appearance as the user scrolls through the list. To avoid this issue, you need to disable
+ * this property.
+ *
+ * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based
+ * solely on the number of items in the adapter and the position of the visible items inside
+ * the adapter. This provides a stable scrollbar as the user navigates through a list of items
+ * with varying widths / heights.
+ *
+ * @param enabled Whether or not to enable smooth scrollbar.
+ *
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public void setSmoothScrollbarEnabled(boolean enabled) {
+ mSmoothScrollbarEnabled = enabled;
+ }
+
+ /**
+ * Returns the current state of the smooth scrollbar feature. It is enabled by default.
+ *
+ * @return True if smooth scrollbar is enabled, false otherwise.
+ *
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public boolean isSmoothScrollbarEnabled() {
+ return mSmoothScrollbarEnabled;
+ }
+
+ private void updateLayoutState(int layoutDirection, int requiredSpace,
+ boolean canUseExistingSpace, RecyclerView.State state) {
+ // If parent provides a hint, don't measure unlimited.
+ mLayoutState.mInfinite = resolveIsInfinite();
+ mLayoutState.mExtra = getExtraLayoutSpace(state);
+ mLayoutState.mLayoutDirection = layoutDirection;
+ int scrollingOffset;
+ if (layoutDirection == LayoutState.LAYOUT_END) {
+ mLayoutState.mExtra += mOrientationHelper.getEndPadding();
+ // get the first child in the direction we are going
+ final View child = getChildClosestToEnd();
+ // the direction in which we are traversing children
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
+ : LayoutState.ITEM_DIRECTION_TAIL;
+ mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
+ mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
+ // calculate how much we can scroll without adding new children (independent of layout)
+ scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
+ - mOrientationHelper.getEndAfterPadding();
+
+ } else {
+ final View child = getChildClosestToStart();
+ mLayoutState.mExtra += mOrientationHelper.getStartAfterPadding();
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
+ : LayoutState.ITEM_DIRECTION_HEAD;
+ mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
+ mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
+ scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
+ + mOrientationHelper.getStartAfterPadding();
+ }
+ mLayoutState.mAvailable = requiredSpace;
+ if (canUseExistingSpace) {
+ mLayoutState.mAvailable -= scrollingOffset;
+ }
+ mLayoutState.mScrollingOffset = scrollingOffset;
+ }
+
+ boolean resolveIsInfinite() {
+ return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED
+ && mOrientationHelper.getEnd() == 0;
+ }
+
+ void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ final int pos = layoutState.mCurrentPosition;
+ if (pos >= 0 && pos < state.getItemCount()) {
+ layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+ }
+ }
+
+ @Override
+ public void collectInitialPrefetchPositions(int adapterItemCount,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ final boolean fromEnd;
+ final int anchorPos;
+ if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
+ // use restored state, since it hasn't been resolved yet
+ fromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+ anchorPos = mPendingSavedState.mAnchorPosition;
+ } else {
+ resolveShouldLayoutReverse();
+ fromEnd = mShouldReverseLayout;
+ if (mPendingScrollPosition == NO_POSITION) {
+ anchorPos = fromEnd ? adapterItemCount - 1 : 0;
+ } else {
+ anchorPos = mPendingScrollPosition;
+ }
+ }
+
+ final int direction = fromEnd
+ ? LayoutState.ITEM_DIRECTION_HEAD
+ : LayoutState.ITEM_DIRECTION_TAIL;
+ int targetPos = anchorPos;
+ for (int i = 0; i < mInitialItemPrefetchCount; i++) {
+ if (targetPos >= 0 && targetPos < adapterItemCount) {
+ layoutPrefetchRegistry.addPosition(targetPos, 0);
+ } else {
+ break; // no more to prefetch
+ }
+ targetPos += direction;
+ }
+ }
+
+ /**
+ * Sets the number of items to prefetch in
+ * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines
+ * how many inner items should be prefetched when this LayoutManager's RecyclerView
+ * is nested inside another RecyclerView.
+ *
+ * <p>Set this value to the number of items this inner LayoutManager will display when it is
+ * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items
+ * so they are ready, avoiding jank as the inner RecyclerView is scrolled into the viewport.</p>
+ *
+ * <p>For example, take a vertically scrolling RecyclerView with horizontally scrolling inner
+ * RecyclerViews. The rows always have 4 items visible in them (or 5 if not aligned). Passing
+ * <code>4</code> to this method for each inner RecyclerView's LinearLayoutManager will enable
+ * RecyclerView's prefetching feature to do create/bind work for 4 views within a row early,
+ * before it is scrolled on screen, instead of just the default 2.</p>
+ *
+ * <p>Calling this method does nothing unless the LayoutManager is in a RecyclerView
+ * nested in another RecyclerView.</p>
+ *
+ * <p class="note"><strong>Note:</strong> Setting this value to be larger than the number of
+ * views that will be visible in this view can incur unnecessary bind work, and an increase to
+ * the number of Views created and in active use.</p>
+ *
+ * @param itemCount Number of items to prefetch
+ *
+ * @see #isItemPrefetchEnabled()
+ * @see #getInitialItemPrefetchCount()
+ * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+ */
+ public void setInitialPrefetchItemCount(int itemCount) {
+ mInitialItemPrefetchCount = itemCount;
+ }
+
+ /**
+ * Gets the number of items to prefetch in
+ * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines
+ * how many inner items should be prefetched when this LayoutManager's RecyclerView
+ * is nested inside another RecyclerView.
+ *
+ * @see #isItemPrefetchEnabled()
+ * @see #setInitialPrefetchItemCount(int)
+ * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+ *
+ * @return number of items to prefetch.
+ */
+ public int getInitialItemPrefetchCount() {
+ return mInitialItemPrefetchCount;
+ }
+
+ @Override
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ int delta = (mOrientation == HORIZONTAL) ? dx : dy;
+ if (getChildCount() == 0 || delta == 0) {
+ // can't support this scroll, so don't bother prefetching
+ return;
+ }
+
+ final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
+ final int absDy = Math.abs(delta);
+ updateLayoutState(layoutDirection, absDy, true, state);
+ collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
+ }
+
+ int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (getChildCount() == 0 || dy == 0) {
+ return 0;
+ }
+ mLayoutState.mRecycle = true;
+ ensureLayoutState();
+ final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
+ final int absDy = Math.abs(dy);
+ updateLayoutState(layoutDirection, absDy, true, state);
+ final int consumed = mLayoutState.mScrollingOffset
+ + fill(recycler, mLayoutState, state, false);
+ if (consumed < 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Don't have any more elements to scroll");
+ }
+ return 0;
+ }
+ final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
+ mOrientationHelper.offsetChildren(-scrolled);
+ if (DEBUG) {
+ Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
+ }
+ mLayoutState.mLastScrollDelta = scrolled;
+ return scrolled;
+ }
+
+ @Override
+ public void assertNotInLayoutOrScroll(String message) {
+ if (mPendingSavedState == null) {
+ super.assertNotInLayoutOrScroll(message);
+ }
+ }
+
+ /**
+ * Recycles children between given indices.
+ *
+ * @param startIndex inclusive
+ * @param endIndex exclusive
+ */
+ private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
+ if (startIndex == endIndex) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
+ }
+ if (endIndex > startIndex) {
+ for (int i = endIndex - 1; i >= startIndex; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ } else {
+ for (int i = startIndex; i > endIndex; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ }
+ }
+
+ /**
+ * Recycles views that went out of bounds after scrolling towards the end of the layout.
+ * <p>
+ * Checks both layout position and visible position to guarantee that the view is not visible.
+ *
+ * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView}
+ * @param dt This can be used to add additional padding to the visible area. This is used
+ * to detect children that will go out of bounds after scrolling, without
+ * actually moving them.
+ */
+ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
+ if (dt < 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Called recycle from start with a negative value. This might happen"
+ + " during layout changes but may be sign of a bug");
+ }
+ return;
+ }
+ // ignore padding, ViewGroup may not clip children.
+ final int limit = dt;
+ final int childCount = getChildCount();
+ if (mShouldReverseLayout) {
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedEnd(child) > limit
+ || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
+ // stop here
+ recycleChildren(recycler, childCount - 1, i);
+ return;
+ }
+ }
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedEnd(child) > limit
+ || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
+ // stop here
+ recycleChildren(recycler, 0, i);
+ return;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Recycles views that went out of bounds after scrolling towards the start of the layout.
+ * <p>
+ * Checks both layout position and visible position to guarantee that the view is not visible.
+ *
+ * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView}
+ * @param dt This can be used to add additional padding to the visible area. This is used
+ * to detect children that will go out of bounds after scrolling, without
+ * actually moving them.
+ */
+ private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) {
+ final int childCount = getChildCount();
+ if (dt < 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Called recycle from end with a negative value. This might happen"
+ + " during layout changes but may be sign of a bug");
+ }
+ return;
+ }
+ final int limit = mOrientationHelper.getEnd() - dt;
+ if (mShouldReverseLayout) {
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedStart(child) < limit
+ || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
+ // stop here
+ recycleChildren(recycler, 0, i);
+ return;
+ }
+ }
+ } else {
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedStart(child) < limit
+ || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
+ // stop here
+ recycleChildren(recycler, childCount - 1, i);
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method to call appropriate recycle method depending on current layout direction
+ *
+ * @param recycler Current recycler that is attached to RecyclerView
+ * @param layoutState Current layout state. Right now, this object does not change but
+ * we may consider moving it out of this view so passing around as a
+ * parameter for now, rather than accessing {@link #mLayoutState}
+ * @see #recycleViewsFromStart(com.android.internal.widget.RecyclerView.Recycler, int)
+ * @see #recycleViewsFromEnd(com.android.internal.widget.RecyclerView.Recycler, int)
+ * @see com.android.internal.widget.LinearLayoutManager.LayoutState#mLayoutDirection
+ */
+ private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
+ if (!layoutState.mRecycle || layoutState.mInfinite) {
+ return;
+ }
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
+ } else {
+ recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
+ }
+ }
+
+ /**
+ * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
+ * independent from the rest of the {@link com.android.internal.widget.LinearLayoutManager}
+ * and with little change, can be made publicly available as a helper class.
+ *
+ * @param recycler Current recycler that is attached to RecyclerView
+ * @param layoutState Configuration on how we should fill out the available space.
+ * @param state Context passed by the RecyclerView to control scroll steps.
+ * @param stopOnFocusable If true, filling stops in the first focusable new child
+ * @return Number of pixels that it added. Useful for scroll functions.
+ */
+ int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
+ RecyclerView.State state, boolean stopOnFocusable) {
+ // max offset we should set is mFastScroll + available
+ final int start = layoutState.mAvailable;
+ if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
+ // TODO ugly bug fix. should not happen
+ if (layoutState.mAvailable < 0) {
+ layoutState.mScrollingOffset += layoutState.mAvailable;
+ }
+ recycleByLayoutState(recycler, layoutState);
+ }
+ int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
+ LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
+ while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
+ layoutChunkResult.resetInternal();
+ layoutChunk(recycler, state, layoutState, layoutChunkResult);
+ if (layoutChunkResult.mFinished) {
+ break;
+ }
+ layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
+ /**
+ * Consume the available space if:
+ * * layoutChunk did not request to be ignored
+ * * OR we are laying out scrap children
+ * * OR we are not doing pre-layout
+ */
+ if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
+ || !state.isPreLayout()) {
+ layoutState.mAvailable -= layoutChunkResult.mConsumed;
+ // we keep a separate remaining space because mAvailable is important for recycling
+ remainingSpace -= layoutChunkResult.mConsumed;
+ }
+
+ if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
+ layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
+ if (layoutState.mAvailable < 0) {
+ layoutState.mScrollingOffset += layoutState.mAvailable;
+ }
+ recycleByLayoutState(recycler, layoutState);
+ }
+ if (stopOnFocusable && layoutChunkResult.mFocusable) {
+ break;
+ }
+ }
+ if (DEBUG) {
+ validateChildOrder();
+ }
+ return start - layoutState.mAvailable;
+ }
+
+ void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
+ LayoutState layoutState, LayoutChunkResult result) {
+ View view = layoutState.next(recycler);
+ if (view == null) {
+ if (DEBUG && layoutState.mScrapList == null) {
+ throw new RuntimeException("received null view when unexpected");
+ }
+ // if we are laying out views in scrap, this may return null which means there is
+ // no more items to layout.
+ result.mFinished = true;
+ return;
+ }
+ LayoutParams params = (LayoutParams) view.getLayoutParams();
+ if (layoutState.mScrapList == null) {
+ if (mShouldReverseLayout == (layoutState.mLayoutDirection
+ == LayoutState.LAYOUT_START)) {
+ addView(view);
+ } else {
+ addView(view, 0);
+ }
+ } else {
+ if (mShouldReverseLayout == (layoutState.mLayoutDirection
+ == LayoutState.LAYOUT_START)) {
+ addDisappearingView(view);
+ } else {
+ addDisappearingView(view, 0);
+ }
+ }
+ measureChildWithMargins(view, 0, 0);
+ result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
+ int left, top, right, bottom;
+ if (mOrientation == VERTICAL) {
+ if (isLayoutRTL()) {
+ right = getWidth() - getPaddingRight();
+ left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
+ } else {
+ left = getPaddingLeft();
+ right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
+ }
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ bottom = layoutState.mOffset;
+ top = layoutState.mOffset - result.mConsumed;
+ } else {
+ top = layoutState.mOffset;
+ bottom = layoutState.mOffset + result.mConsumed;
+ }
+ } else {
+ top = getPaddingTop();
+ bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
+
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ right = layoutState.mOffset;
+ left = layoutState.mOffset - result.mConsumed;
+ } else {
+ left = layoutState.mOffset;
+ right = layoutState.mOffset + result.mConsumed;
+ }
+ }
+ // We calculate everything with View's bounding box (which includes decor and margins)
+ // To calculate correct layout position, we subtract margins.
+ layoutDecoratedWithMargins(view, left, top, right, bottom);
+ if (DEBUG) {
+ Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
+ }
+ // Consume the available space if the view is not removed OR changed
+ if (params.isItemRemoved() || params.isItemChanged()) {
+ result.mIgnoreConsumed = true;
+ }
+ result.mFocusable = view.isFocusable();
+ }
+
+ @Override
+ boolean shouldMeasureTwice() {
+ return getHeightMode() != View.MeasureSpec.EXACTLY
+ && getWidthMode() != View.MeasureSpec.EXACTLY
+ && hasFlexibleChildInBothOrientations();
+ }
+
+ /**
+ * Converts a focusDirection to orientation.
+ *
+ * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+ * or 0 for not applicable
+ * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction
+ * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise.
+ */
+ int convertFocusDirectionToLayoutDirection(int focusDirection) {
+ switch (focusDirection) {
+ case View.FOCUS_BACKWARD:
+ if (mOrientation == VERTICAL) {
+ return LayoutState.LAYOUT_START;
+ } else if (isLayoutRTL()) {
+ return LayoutState.LAYOUT_END;
+ } else {
+ return LayoutState.LAYOUT_START;
+ }
+ case View.FOCUS_FORWARD:
+ if (mOrientation == VERTICAL) {
+ return LayoutState.LAYOUT_END;
+ } else if (isLayoutRTL()) {
+ return LayoutState.LAYOUT_START;
+ } else {
+ return LayoutState.LAYOUT_END;
+ }
+ case View.FOCUS_UP:
+ return mOrientation == VERTICAL ? LayoutState.LAYOUT_START
+ : LayoutState.INVALID_LAYOUT;
+ case View.FOCUS_DOWN:
+ return mOrientation == VERTICAL ? LayoutState.LAYOUT_END
+ : LayoutState.INVALID_LAYOUT;
+ case View.FOCUS_LEFT:
+ return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START
+ : LayoutState.INVALID_LAYOUT;
+ case View.FOCUS_RIGHT:
+ return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END
+ : LayoutState.INVALID_LAYOUT;
+ default:
+ if (DEBUG) {
+ Log.d(TAG, "Unknown focus request:" + focusDirection);
+ }
+ return LayoutState.INVALID_LAYOUT;
+ }
+
+ }
+
+ /**
+ * Convenience method to find the child closes to start. Caller should check it has enough
+ * children.
+ *
+ * @return The child closes to start of the layout from user's perspective.
+ */
+ private View getChildClosestToStart() {
+ return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0);
+ }
+
+ /**
+ * Convenience method to find the child closes to end. Caller should check it has enough
+ * children.
+ *
+ * @return The child closes to end of the layout from user's perspective.
+ */
+ private View getChildClosestToEnd() {
+ return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1);
+ }
+
+ /**
+ * Convenience method to find the visible child closes to start. Caller should check if it has
+ * enough children.
+ *
+ * @param completelyVisible Whether child should be completely visible or not
+ * @return The first visible child closest to start of the layout from user's perspective.
+ */
+ private View findFirstVisibleChildClosestToStart(boolean completelyVisible,
+ boolean acceptPartiallyVisible) {
+ if (mShouldReverseLayout) {
+ return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible,
+ acceptPartiallyVisible);
+ } else {
+ return findOneVisibleChild(0, getChildCount(), completelyVisible,
+ acceptPartiallyVisible);
+ }
+ }
+
+ /**
+ * Convenience method to find the visible child closes to end. Caller should check if it has
+ * enough children.
+ *
+ * @param completelyVisible Whether child should be completely visible or not
+ * @return The first visible child closest to end of the layout from user's perspective.
+ */
+ private View findFirstVisibleChildClosestToEnd(boolean completelyVisible,
+ boolean acceptPartiallyVisible) {
+ if (mShouldReverseLayout) {
+ return findOneVisibleChild(0, getChildCount(), completelyVisible,
+ acceptPartiallyVisible);
+ } else {
+ return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible,
+ acceptPartiallyVisible);
+ }
+ }
+
+
+ /**
+ * Among the children that are suitable to be considered as an anchor child, returns the one
+ * closest to the end of the layout.
+ * <p>
+ * Due to ambiguous adapter updates or children being removed, some children's positions may be
+ * invalid. This method is a best effort to find a position within adapter bounds if possible.
+ * <p>
+ * It also prioritizes children that are within the visible bounds.
+ * @return A View that can be used an an anchor View.
+ */
+ private View findReferenceChildClosestToEnd(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ return mShouldReverseLayout ? findFirstReferenceChild(recycler, state) :
+ findLastReferenceChild(recycler, state);
+ }
+
+ /**
+ * Among the children that are suitable to be considered as an anchor child, returns the one
+ * closest to the start of the layout.
+ * <p>
+ * Due to ambiguous adapter updates or children being removed, some children's positions may be
+ * invalid. This method is a best effort to find a position within adapter bounds if possible.
+ * <p>
+ * It also prioritizes children that are within the visible bounds.
+ *
+ * @return A View that can be used an an anchor View.
+ */
+ private View findReferenceChildClosestToStart(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ return mShouldReverseLayout ? findLastReferenceChild(recycler, state) :
+ findFirstReferenceChild(recycler, state);
+ }
+
+ private View findFirstReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ return findReferenceChild(recycler, state, 0, getChildCount(), state.getItemCount());
+ }
+
+ private View findLastReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ return findReferenceChild(recycler, state, getChildCount() - 1, -1, state.getItemCount());
+ }
+
+ // overridden by GridLayoutManager
+ View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
+ int start, int end, int itemCount) {
+ ensureLayoutState();
+ View invalidMatch = null;
+ View outOfBoundsMatch = null;
+ final int boundsStart = mOrientationHelper.getStartAfterPadding();
+ final int boundsEnd = mOrientationHelper.getEndAfterPadding();
+ final int diff = end > start ? 1 : -1;
+ for (int i = start; i != end; i += diff) {
+ final View view = getChildAt(i);
+ final int position = getPosition(view);
+ if (position >= 0 && position < itemCount) {
+ if (((LayoutParams) view.getLayoutParams()).isItemRemoved()) {
+ if (invalidMatch == null) {
+ invalidMatch = view; // removed item, least preferred
+ }
+ } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd
+ || mOrientationHelper.getDecoratedEnd(view) < boundsStart) {
+ if (outOfBoundsMatch == null) {
+ outOfBoundsMatch = view; // item is not visible, less preferred
+ }
+ } else {
+ return view;
+ }
+ }
+ }
+ return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
+ }
+
+ /**
+ * Returns the adapter position of the first visible view. This position does not include
+ * adapter changes that were dispatched after the last layout pass.
+ * <p>
+ * Note that, this value is not affected by layout orientation or item order traversal.
+ * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
+ * not in the layout.
+ * <p>
+ * If RecyclerView has item decorators, they will be considered in calculations as well.
+ * <p>
+ * LayoutManager may pre-cache some views that are not necessarily visible. Those views
+ * are ignored in this method.
+ *
+ * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if
+ * there aren't any visible items.
+ * @see #findFirstCompletelyVisibleItemPosition()
+ * @see #findLastVisibleItemPosition()
+ */
+ public int findFirstVisibleItemPosition() {
+ final View child = findOneVisibleChild(0, getChildCount(), false, true);
+ return child == null ? NO_POSITION : getPosition(child);
+ }
+
+ /**
+ * Returns the adapter position of the first fully visible view. This position does not include
+ * adapter changes that were dispatched after the last layout pass.
+ * <p>
+ * Note that bounds check is only performed in the current orientation. That means, if
+ * LayoutManager is horizontal, it will only check the view's left and right edges.
+ *
+ * @return The adapter position of the first fully visible item or
+ * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
+ * @see #findFirstVisibleItemPosition()
+ * @see #findLastCompletelyVisibleItemPosition()
+ */
+ public int findFirstCompletelyVisibleItemPosition() {
+ final View child = findOneVisibleChild(0, getChildCount(), true, false);
+ return child == null ? NO_POSITION : getPosition(child);
+ }
+
+ /**
+ * Returns the adapter position of the last visible view. This position does not include
+ * adapter changes that were dispatched after the last layout pass.
+ * <p>
+ * Note that, this value is not affected by layout orientation or item order traversal.
+ * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
+ * not in the layout.
+ * <p>
+ * If RecyclerView has item decorators, they will be considered in calculations as well.
+ * <p>
+ * LayoutManager may pre-cache some views that are not necessarily visible. Those views
+ * are ignored in this method.
+ *
+ * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if
+ * there aren't any visible items.
+ * @see #findLastCompletelyVisibleItemPosition()
+ * @see #findFirstVisibleItemPosition()
+ */
+ public int findLastVisibleItemPosition() {
+ final View child = findOneVisibleChild(getChildCount() - 1, -1, false, true);
+ return child == null ? NO_POSITION : getPosition(child);
+ }
+
+ /**
+ * Returns the adapter position of the last fully visible view. This position does not include
+ * adapter changes that were dispatched after the last layout pass.
+ * <p>
+ * Note that bounds check is only performed in the current orientation. That means, if
+ * LayoutManager is horizontal, it will only check the view's left and right edges.
+ *
+ * @return The adapter position of the last fully visible view or
+ * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
+ * @see #findLastVisibleItemPosition()
+ * @see #findFirstCompletelyVisibleItemPosition()
+ */
+ public int findLastCompletelyVisibleItemPosition() {
+ final View child = findOneVisibleChild(getChildCount() - 1, -1, true, false);
+ return child == null ? NO_POSITION : getPosition(child);
+ }
+
+ View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible,
+ boolean acceptPartiallyVisible) {
+ ensureLayoutState();
+ final int start = mOrientationHelper.getStartAfterPadding();
+ final int end = mOrientationHelper.getEndAfterPadding();
+ final int next = toIndex > fromIndex ? 1 : -1;
+ View partiallyVisible = null;
+ for (int i = fromIndex; i != toIndex; i += next) {
+ final View child = getChildAt(i);
+ final int childStart = mOrientationHelper.getDecoratedStart(child);
+ final int childEnd = mOrientationHelper.getDecoratedEnd(child);
+ if (childStart < end && childEnd > start) {
+ if (completelyVisible) {
+ if (childStart >= start && childEnd <= end) {
+ return child;
+ } else if (acceptPartiallyVisible && partiallyVisible == null) {
+ partiallyVisible = child;
+ }
+ } else {
+ return child;
+ }
+ }
+ }
+ return partiallyVisible;
+ }
+
+ @Override
+ public View onFocusSearchFailed(View focused, int focusDirection,
+ RecyclerView.Recycler recycler, RecyclerView.State state) {
+ resolveShouldLayoutReverse();
+ if (getChildCount() == 0) {
+ return null;
+ }
+
+ final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection);
+ if (layoutDir == LayoutState.INVALID_LAYOUT) {
+ return null;
+ }
+ ensureLayoutState();
+ final View referenceChild;
+ if (layoutDir == LayoutState.LAYOUT_START) {
+ referenceChild = findReferenceChildClosestToStart(recycler, state);
+ } else {
+ referenceChild = findReferenceChildClosestToEnd(recycler, state);
+ }
+ if (referenceChild == null) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "Cannot find a child with a valid position to be used for focus search.");
+ }
+ return null;
+ }
+ ensureLayoutState();
+ final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());
+ updateLayoutState(layoutDir, maxScroll, false, state);
+ mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
+ mLayoutState.mRecycle = false;
+ fill(recycler, mLayoutState, state, true);
+ final View nextFocus;
+ if (layoutDir == LayoutState.LAYOUT_START) {
+ nextFocus = getChildClosestToStart();
+ } else {
+ nextFocus = getChildClosestToEnd();
+ }
+ if (nextFocus == referenceChild || !nextFocus.isFocusable()) {
+ return null;
+ }
+ return nextFocus;
+ }
+
+ /**
+ * Used for debugging.
+ * Logs the internal representation of children to default logger.
+ */
+ private void logChildren() {
+ Log.d(TAG, "internal representation of views on the screen");
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ Log.d(TAG, "item " + getPosition(child) + ", coord:"
+ + mOrientationHelper.getDecoratedStart(child));
+ }
+ Log.d(TAG, "==============");
+ }
+
+ /**
+ * Used for debugging.
+ * Validates that child views are laid out in correct order. This is important because rest of
+ * the algorithm relies on this constraint.
+ *
+ * In default layout, child 0 should be closest to screen position 0 and last child should be
+ * closest to position WIDTH or HEIGHT.
+ * In reverse layout, last child should be closes to screen position 0 and first child should
+ * be closest to position WIDTH or HEIGHT
+ */
+ void validateChildOrder() {
+ Log.d(TAG, "validating child count " + getChildCount());
+ if (getChildCount() < 1) {
+ return;
+ }
+ int lastPos = getPosition(getChildAt(0));
+ int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0));
+ if (mShouldReverseLayout) {
+ for (int i = 1; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ int pos = getPosition(child);
+ int screenLoc = mOrientationHelper.getDecoratedStart(child);
+ if (pos < lastPos) {
+ logChildren();
+ throw new RuntimeException("detected invalid position. loc invalid? "
+ + (screenLoc < lastScreenLoc));
+ }
+ if (screenLoc > lastScreenLoc) {
+ logChildren();
+ throw new RuntimeException("detected invalid location");
+ }
+ }
+ } else {
+ for (int i = 1; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ int pos = getPosition(child);
+ int screenLoc = mOrientationHelper.getDecoratedStart(child);
+ if (pos < lastPos) {
+ logChildren();
+ throw new RuntimeException("detected invalid position. loc invalid? "
+ + (screenLoc < lastScreenLoc));
+ }
+ if (screenLoc < lastScreenLoc) {
+ logChildren();
+ throw new RuntimeException("detected invalid location");
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd;
+ }
+
+ /**
+ * @hide This method should be called by ItemTouchHelper only.
+ */
+ @Override
+ public void prepareForDrop(View view, View target, int x, int y) {
+ assertNotInLayoutOrScroll("Cannot drop a view during a scroll or layout calculation");
+ ensureLayoutState();
+ resolveShouldLayoutReverse();
+ final int myPos = getPosition(view);
+ final int targetPos = getPosition(target);
+ final int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL
+ : LayoutState.ITEM_DIRECTION_HEAD;
+ if (mShouldReverseLayout) {
+ if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) {
+ scrollToPositionWithOffset(targetPos,
+ mOrientationHelper.getEndAfterPadding()
+ - (mOrientationHelper.getDecoratedStart(target)
+ + mOrientationHelper.getDecoratedMeasurement(view)));
+ } else {
+ scrollToPositionWithOffset(targetPos,
+ mOrientationHelper.getEndAfterPadding()
+ - mOrientationHelper.getDecoratedEnd(target));
+ }
+ } else {
+ if (dropDirection == LayoutState.ITEM_DIRECTION_HEAD) {
+ scrollToPositionWithOffset(targetPos, mOrientationHelper.getDecoratedStart(target));
+ } else {
+ scrollToPositionWithOffset(targetPos,
+ mOrientationHelper.getDecoratedEnd(target)
+ - mOrientationHelper.getDecoratedMeasurement(view));
+ }
+ }
+ }
+
+ /**
+ * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
+ * space.
+ */
+ static class LayoutState {
+
+ static final String TAG = "LLM#LayoutState";
+
+ static final int LAYOUT_START = -1;
+
+ static final int LAYOUT_END = 1;
+
+ static final int INVALID_LAYOUT = Integer.MIN_VALUE;
+
+ static final int ITEM_DIRECTION_HEAD = -1;
+
+ static final int ITEM_DIRECTION_TAIL = 1;
+
+ static final int SCROLLING_OFFSET_NaN = Integer.MIN_VALUE;
+
+ /**
+ * We may not want to recycle children in some cases (e.g. layout)
+ */
+ boolean mRecycle = true;
+
+ /**
+ * Pixel offset where layout should start
+ */
+ int mOffset;
+
+ /**
+ * Number of pixels that we should fill, in the layout direction.
+ */
+ int mAvailable;
+
+ /**
+ * Current position on the adapter to get the next item.
+ */
+ int mCurrentPosition;
+
+ /**
+ * Defines the direction in which the data adapter is traversed.
+ * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
+ */
+ int mItemDirection;
+
+ /**
+ * Defines the direction in which the layout is filled.
+ * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
+ */
+ int mLayoutDirection;
+
+ /**
+ * Used when LayoutState is constructed in a scrolling state.
+ * It should be set the amount of scrolling we can make without creating a new view.
+ * Settings this is required for efficient view recycling.
+ */
+ int mScrollingOffset;
+
+ /**
+ * Used if you want to pre-layout items that are not yet visible.
+ * The difference with {@link #mAvailable} is that, when recycling, distance laid out for
+ * {@link #mExtra} is not considered to avoid recycling visible children.
+ */
+ int mExtra = 0;
+
+ /**
+ * Equal to {@link RecyclerView.State#isPreLayout()}. When consuming scrap, if this value
+ * is set to true, we skip removed views since they should not be laid out in post layout
+ * step.
+ */
+ boolean mIsPreLayout = false;
+
+ /**
+ * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)}
+ * amount.
+ */
+ int mLastScrollDelta;
+
+ /**
+ * When LLM needs to layout particular views, it sets this list in which case, LayoutState
+ * will only return views from this list and return null if it cannot find an item.
+ */
+ List<RecyclerView.ViewHolder> mScrapList = null;
+
+ /**
+ * Used when there is no limit in how many views can be laid out.
+ */
+ boolean mInfinite;
+
+ /**
+ * @return true if there are more items in the data adapter
+ */
+ boolean hasMore(RecyclerView.State state) {
+ return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
+ }
+
+ /**
+ * Gets the view for the next element that we should layout.
+ * Also updates current item index to the next item, based on {@link #mItemDirection}
+ *
+ * @return The next element that we should layout.
+ */
+ View next(RecyclerView.Recycler recycler) {
+ if (mScrapList != null) {
+ return nextViewFromScrapList();
+ }
+ final View view = recycler.getViewForPosition(mCurrentPosition);
+ mCurrentPosition += mItemDirection;
+ return view;
+ }
+
+ /**
+ * Returns the next item from the scrap list.
+ * <p>
+ * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection
+ *
+ * @return View if an item in the current position or direction exists if not null.
+ */
+ private View nextViewFromScrapList() {
+ final int size = mScrapList.size();
+ for (int i = 0; i < size; i++) {
+ final View view = mScrapList.get(i).itemView;
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (lp.isItemRemoved()) {
+ continue;
+ }
+ if (mCurrentPosition == lp.getViewLayoutPosition()) {
+ assignPositionFromScrapList(view);
+ return view;
+ }
+ }
+ return null;
+ }
+
+ public void assignPositionFromScrapList() {
+ assignPositionFromScrapList(null);
+ }
+
+ public void assignPositionFromScrapList(View ignore) {
+ final View closest = nextViewInLimitedList(ignore);
+ if (closest == null) {
+ mCurrentPosition = NO_POSITION;
+ } else {
+ mCurrentPosition = ((LayoutParams) closest.getLayoutParams())
+ .getViewLayoutPosition();
+ }
+ }
+
+ public View nextViewInLimitedList(View ignore) {
+ int size = mScrapList.size();
+ View closest = null;
+ int closestDistance = Integer.MAX_VALUE;
+ if (DEBUG && mIsPreLayout) {
+ throw new IllegalStateException("Scrap list cannot be used in pre layout");
+ }
+ for (int i = 0; i < size; i++) {
+ View view = mScrapList.get(i).itemView;
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (view == ignore || lp.isItemRemoved()) {
+ continue;
+ }
+ final int distance = (lp.getViewLayoutPosition() - mCurrentPosition)
+ * mItemDirection;
+ if (distance < 0) {
+ continue; // item is not in current direction
+ }
+ if (distance < closestDistance) {
+ closest = view;
+ closestDistance = distance;
+ if (distance == 0) {
+ break;
+ }
+ }
+ }
+ return closest;
+ }
+
+ void log() {
+ Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:"
+ + mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class SavedState implements Parcelable {
+
+ int mAnchorPosition;
+
+ int mAnchorOffset;
+
+ boolean mAnchorLayoutFromEnd;
+
+ public SavedState() {
+
+ }
+
+ SavedState(Parcel in) {
+ mAnchorPosition = in.readInt();
+ mAnchorOffset = in.readInt();
+ mAnchorLayoutFromEnd = in.readInt() == 1;
+ }
+
+ public SavedState(SavedState other) {
+ mAnchorPosition = other.mAnchorPosition;
+ mAnchorOffset = other.mAnchorOffset;
+ mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
+ }
+
+ boolean hasValidAnchor() {
+ return mAnchorPosition >= 0;
+ }
+
+ void invalidateAnchor() {
+ mAnchorPosition = NO_POSITION;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mAnchorPosition);
+ dest.writeInt(mAnchorOffset);
+ dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ /**
+ * Simple data class to keep Anchor information
+ */
+ class AnchorInfo {
+ int mPosition;
+ int mCoordinate;
+ boolean mLayoutFromEnd;
+ boolean mValid;
+
+ AnchorInfo() {
+ reset();
+ }
+
+ void reset() {
+ mPosition = NO_POSITION;
+ mCoordinate = INVALID_OFFSET;
+ mLayoutFromEnd = false;
+ mValid = false;
+ }
+
+ /**
+ * assigns anchor coordinate from the RecyclerView's padding depending on current
+ * layoutFromEnd value
+ */
+ void assignCoordinateFromPadding() {
+ mCoordinate = mLayoutFromEnd
+ ? mOrientationHelper.getEndAfterPadding()
+ : mOrientationHelper.getStartAfterPadding();
+ }
+
+ @Override
+ public String toString() {
+ return "AnchorInfo{"
+ + "mPosition=" + mPosition
+ + ", mCoordinate=" + mCoordinate
+ + ", mLayoutFromEnd=" + mLayoutFromEnd
+ + ", mValid=" + mValid
+ + '}';
+ }
+
+ boolean isViewValidAsAnchor(View child, RecyclerView.State state) {
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0
+ && lp.getViewLayoutPosition() < state.getItemCount();
+ }
+
+ public void assignFromViewAndKeepVisibleRect(View child) {
+ final int spaceChange = mOrientationHelper.getTotalSpaceChange();
+ if (spaceChange >= 0) {
+ assignFromView(child);
+ return;
+ }
+ mPosition = getPosition(child);
+ if (mLayoutFromEnd) {
+ final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange;
+ final int childEnd = mOrientationHelper.getDecoratedEnd(child);
+ final int previousEndMargin = prevLayoutEnd - childEnd;
+ mCoordinate = mOrientationHelper.getEndAfterPadding() - previousEndMargin;
+ // ensure we did not push child's top out of bounds because of this
+ if (previousEndMargin > 0) { // we have room to shift bottom if necessary
+ final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
+ final int estimatedChildStart = mCoordinate - childSize;
+ final int layoutStart = mOrientationHelper.getStartAfterPadding();
+ final int previousStartMargin = mOrientationHelper.getDecoratedStart(child)
+ - layoutStart;
+ final int startReference = layoutStart + Math.min(previousStartMargin, 0);
+ final int startMargin = estimatedChildStart - startReference;
+ if (startMargin < 0) {
+ // offset to make top visible but not too much
+ mCoordinate += Math.min(previousEndMargin, -startMargin);
+ }
+ }
+ } else {
+ final int childStart = mOrientationHelper.getDecoratedStart(child);
+ final int startMargin = childStart - mOrientationHelper.getStartAfterPadding();
+ mCoordinate = childStart;
+ if (startMargin > 0) { // we have room to fix end as well
+ final int estimatedEnd = childStart
+ + mOrientationHelper.getDecoratedMeasurement(child);
+ final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding()
+ - spaceChange;
+ final int previousEndMargin = previousLayoutEnd
+ - mOrientationHelper.getDecoratedEnd(child);
+ final int endReference = mOrientationHelper.getEndAfterPadding()
+ - Math.min(0, previousEndMargin);
+ final int endMargin = endReference - estimatedEnd;
+ if (endMargin < 0) {
+ mCoordinate -= Math.min(startMargin, -endMargin);
+ }
+ }
+ }
+ }
+
+ public void assignFromView(View child) {
+ if (mLayoutFromEnd) {
+ mCoordinate = mOrientationHelper.getDecoratedEnd(child)
+ + mOrientationHelper.getTotalSpaceChange();
+ } else {
+ mCoordinate = mOrientationHelper.getDecoratedStart(child);
+ }
+
+ mPosition = getPosition(child);
+ }
+ }
+
+ protected static class LayoutChunkResult {
+ public int mConsumed;
+ public boolean mFinished;
+ public boolean mIgnoreConsumed;
+ public boolean mFocusable;
+
+ void resetInternal() {
+ mConsumed = 0;
+ mFinished = false;
+ mIgnoreConsumed = false;
+ mFocusable = false;
+ }
+ }
+}
diff --git a/com/android/internal/widget/LinearLayoutWithDefaultTouchRecepient.java b/com/android/internal/widget/LinearLayoutWithDefaultTouchRecepient.java
new file mode 100644
index 0000000..9ef9f69
--- /dev/null
+++ b/com/android/internal/widget/LinearLayoutWithDefaultTouchRecepient.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2008 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.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+
+
+/**
+ * Like a normal linear layout, but supports dispatching all otherwise unhandled
+ * touch events to a particular descendant. This is for the unlock screen, so
+ * that a wider range of touch events than just the lock pattern widget can kick
+ * off a lock pattern if the finger is eventually dragged into the bounds of the
+ * lock pattern view.
+ */
+public class LinearLayoutWithDefaultTouchRecepient extends LinearLayout {
+
+ private final Rect mTempRect = new Rect();
+ private View mDefaultTouchRecepient;
+
+ @UnsupportedAppUsage
+ public LinearLayoutWithDefaultTouchRecepient(Context context) {
+ super(context);
+ }
+
+ public LinearLayoutWithDefaultTouchRecepient(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @UnsupportedAppUsage
+ public void setDefaultTouchRecepient(View defaultTouchRecepient) {
+ mDefaultTouchRecepient = defaultTouchRecepient;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mDefaultTouchRecepient == null) {
+ return super.dispatchTouchEvent(ev);
+ }
+
+ if (super.dispatchTouchEvent(ev)) {
+ return true;
+ }
+ mTempRect.set(0, 0, 0, 0);
+ offsetRectIntoDescendantCoords(mDefaultTouchRecepient, mTempRect);
+ ev.setLocation(ev.getX() + mTempRect.left, ev.getY() + mTempRect.top);
+ return mDefaultTouchRecepient.dispatchTouchEvent(ev);
+ }
+
+}
diff --git a/com/android/internal/widget/LinearSmoothScroller.java b/com/android/internal/widget/LinearSmoothScroller.java
new file mode 100644
index 0000000..d024f21
--- /dev/null
+++ b/com/android/internal/widget/LinearSmoothScroller.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+
+/**
+ * {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until
+ * the target position becomes a child of the RecyclerView and then uses a
+ * {@link DecelerateInterpolator} to slowly approach to target position.
+ * <p>
+ * If the {@link RecyclerView.LayoutManager} you are using does not implement the
+ * {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface, then you must override the
+ * {@link #computeScrollVectorForPosition(int)} method. All the LayoutManagers bundled with
+ * the support library implement this interface.
+ */
+public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
+
+ private static final String TAG = "LinearSmoothScroller";
+
+ private static final boolean DEBUG = false;
+
+ private static final float MILLISECONDS_PER_INCH = 25f;
+
+ private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
+
+ /**
+ * Align child view's left or top with parent view's left or top
+ *
+ * @see #calculateDtToFit(int, int, int, int, int)
+ * @see #calculateDxToMakeVisible(android.view.View, int)
+ * @see #calculateDyToMakeVisible(android.view.View, int)
+ */
+ public static final int SNAP_TO_START = -1;
+
+ /**
+ * Align child view's right or bottom with parent view's right or bottom
+ *
+ * @see #calculateDtToFit(int, int, int, int, int)
+ * @see #calculateDxToMakeVisible(android.view.View, int)
+ * @see #calculateDyToMakeVisible(android.view.View, int)
+ */
+ public static final int SNAP_TO_END = 1;
+
+ /**
+ * <p>Decides if the child should be snapped from start or end, depending on where it
+ * currently is in relation to its parent.</p>
+ * <p>For instance, if the view is virtually on the left of RecyclerView, using
+ * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
+ *
+ * @see #calculateDtToFit(int, int, int, int, int)
+ * @see #calculateDxToMakeVisible(android.view.View, int)
+ * @see #calculateDyToMakeVisible(android.view.View, int)
+ */
+ public static final int SNAP_TO_ANY = 0;
+
+ // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
+ // view is not laid out until interim target position is reached, we can detect the case before
+ // scrolling slows down and reschedule another interim target scroll
+ private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
+
+ protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
+
+ protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
+
+ protected PointF mTargetVector;
+
+ private final float MILLISECONDS_PER_PX;
+
+ // Temporary variables to keep track of the interim scroll target. These values do not
+ // point to a real item position, rather point to an estimated location pixels.
+ protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
+
+ public LinearSmoothScroller(Context context) {
+ MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+ final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
+ final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
+ final int distance = (int) Math.sqrt(dx * dx + dy * dy);
+ final int time = calculateTimeForDeceleration(distance);
+ if (time > 0) {
+ action.update(-dx, -dy, time, mDecelerateInterpolator);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
+ if (getChildCount() == 0) {
+ stop();
+ return;
+ }
+ //noinspection PointlessBooleanExpression
+ if (DEBUG && mTargetVector != null
+ && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
+ throw new IllegalStateException("Scroll happened in the opposite direction"
+ + " of the target. Some calculations are wrong");
+ }
+ mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
+ mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
+
+ if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
+ updateActionForInterimTarget(action);
+ } // everything is valid, keep going
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStop() {
+ mInterimTargetDx = mInterimTargetDy = 0;
+ mTargetVector = null;
+ }
+
+ /**
+ * Calculates the scroll speed.
+ *
+ * @param displayMetrics DisplayMetrics to be used for real dimension calculations
+ * @return The time (in ms) it should take for each pixel. For instance, if returned value is
+ * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
+ */
+ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+ return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
+ }
+
+ /**
+ * <p>Calculates the time for deceleration so that transition from LinearInterpolator to
+ * DecelerateInterpolator looks smooth.</p>
+ *
+ * @param dx Distance to scroll
+ * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
+ * from LinearInterpolation
+ */
+ protected int calculateTimeForDeceleration(int dx) {
+ // we want to cover same area with the linear interpolator for the first 10% of the
+ // interpolation. After that, deceleration will take control.
+ // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
+ // which gives 0.100028 when x = .3356
+ // this is why we divide linear scrolling time with .3356
+ return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
+ }
+
+ /**
+ * Calculates the time it should take to scroll the given distance (in pixels)
+ *
+ * @param dx Distance in pixels that we want to scroll
+ * @return Time in milliseconds
+ * @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
+ */
+ protected int calculateTimeForScrolling(int dx) {
+ // In a case where dx is very small, rounding may return 0 although dx > 0.
+ // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
+ // time.
+ return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
+ }
+
+ /**
+ * When scrolling towards a child view, this method defines whether we should align the left
+ * or the right edge of the child with the parent RecyclerView.
+ *
+ * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
+ * @see #SNAP_TO_START
+ * @see #SNAP_TO_END
+ * @see #SNAP_TO_ANY
+ */
+ protected int getHorizontalSnapPreference() {
+ return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
+ mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
+ }
+
+ /**
+ * When scrolling towards a child view, this method defines whether we should align the top
+ * or the bottom edge of the child with the parent RecyclerView.
+ *
+ * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
+ * @see #SNAP_TO_START
+ * @see #SNAP_TO_END
+ * @see #SNAP_TO_ANY
+ */
+ protected int getVerticalSnapPreference() {
+ return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
+ mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
+ }
+
+ /**
+ * When the target scroll position is not a child of the RecyclerView, this method calculates
+ * a direction vector towards that child and triggers a smooth scroll.
+ *
+ * @see #computeScrollVectorForPosition(int)
+ */
+ protected void updateActionForInterimTarget(Action action) {
+ // find an interim target position
+ PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
+ if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
+ final int target = getTargetPosition();
+ action.jumpTo(target);
+ stop();
+ return;
+ }
+ normalize(scrollVector);
+ mTargetVector = scrollVector;
+
+ mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
+ mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
+ final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
+ // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
+ // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
+ // won't actually scroll more than what we need.
+ action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
+ (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
+ (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
+ }
+
+ private int clampApplyScroll(int tmpDt, int dt) {
+ final int before = tmpDt;
+ tmpDt -= dt;
+ if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
+ return 0;
+ }
+ return tmpDt;
+ }
+
+ /**
+ * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
+ * {@link #calculateDyToMakeVisible(android.view.View, int)}
+ */
+ public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
+ snapPreference) {
+ switch (snapPreference) {
+ case SNAP_TO_START:
+ return boxStart - viewStart;
+ case SNAP_TO_END:
+ return boxEnd - viewEnd;
+ case SNAP_TO_ANY:
+ final int dtStart = boxStart - viewStart;
+ if (dtStart > 0) {
+ return dtStart;
+ }
+ final int dtEnd = boxEnd - viewEnd;
+ if (dtEnd < 0) {
+ return dtEnd;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("snap preference should be one of the"
+ + " constants defined in SmoothScroller, starting with SNAP_");
+ }
+ return 0;
+ }
+
+ /**
+ * Calculates the vertical scroll amount necessary to make the given view fully visible
+ * inside the RecyclerView.
+ *
+ * @param view The view which we want to make fully visible
+ * @param snapPreference The edge which the view should snap to when entering the visible
+ * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
+ * {@link #SNAP_TO_ANY}.
+ * @return The vertical scroll amount necessary to make the view visible with the given
+ * snap preference.
+ */
+ public int calculateDyToMakeVisible(View view, int snapPreference) {
+ final RecyclerView.LayoutManager layoutManager = getLayoutManager();
+ if (layoutManager == null || !layoutManager.canScrollVertically()) {
+ return 0;
+ }
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
+ final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
+ final int start = layoutManager.getPaddingTop();
+ final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
+ return calculateDtToFit(top, bottom, start, end, snapPreference);
+ }
+
+ /**
+ * Calculates the horizontal scroll amount necessary to make the given view fully visible
+ * inside the RecyclerView.
+ *
+ * @param view The view which we want to make fully visible
+ * @param snapPreference The edge which the view should snap to when entering the visible
+ * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
+ * {@link #SNAP_TO_END}
+ * @return The vertical scroll amount necessary to make the view visible with the given
+ * snap preference.
+ */
+ public int calculateDxToMakeVisible(View view, int snapPreference) {
+ final RecyclerView.LayoutManager layoutManager = getLayoutManager();
+ if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
+ return 0;
+ }
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
+ final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
+ final int start = layoutManager.getPaddingLeft();
+ final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
+ return calculateDtToFit(left, right, start, end, snapPreference);
+ }
+
+ /**
+ * Compute the scroll vector for a given target position.
+ * <p>
+ * This method can return null if the layout manager cannot calculate a scroll vector
+ * for the given position (e.g. it has no current scroll position).
+ *
+ * @param targetPosition the position to which the scroller is scrolling
+ *
+ * @return the scroll vector for a given target position
+ */
+ @Nullable
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ RecyclerView.LayoutManager layoutManager = getLayoutManager();
+ if (layoutManager instanceof ScrollVectorProvider) {
+ return ((ScrollVectorProvider) layoutManager)
+ .computeScrollVectorForPosition(targetPosition);
+ }
+ Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager"
+ + " does not implement " + ScrollVectorProvider.class.getCanonicalName());
+ return null;
+ }
+}
diff --git a/com/android/internal/widget/LocalImageResolver.java b/com/android/internal/widget/LocalImageResolver.java
new file mode 100644
index 0000000..2302de2
--- /dev/null
+++ b/com/android/internal/widget/LocalImageResolver.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A class to extract Bitmaps from a MessagingStyle message.
+ */
+public class LocalImageResolver {
+
+ private static final int MAX_SAFE_ICON_SIZE_PX = 480;
+
+ @Nullable
+ public static Drawable resolveImage(Uri uri, Context context) throws IOException {
+ BitmapFactory.Options onlyBoundsOptions = getBoundsOptionsForImage(uri, context);
+ if ((onlyBoundsOptions.outWidth == -1) || (onlyBoundsOptions.outHeight == -1)) {
+ return null;
+ }
+
+ int originalSize =
+ (onlyBoundsOptions.outHeight > onlyBoundsOptions.outWidth)
+ ? onlyBoundsOptions.outHeight
+ : onlyBoundsOptions.outWidth;
+
+ double ratio = (originalSize > MAX_SAFE_ICON_SIZE_PX)
+ ? (originalSize / MAX_SAFE_ICON_SIZE_PX)
+ : 1.0;
+
+ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+ bitmapOptions.inSampleSize = getPowerOfTwoForSampleRatio(ratio);
+ InputStream input = context.getContentResolver().openInputStream(uri);
+ Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
+ input.close();
+ return new BitmapDrawable(context.getResources(), bitmap);
+ }
+
+ private static BitmapFactory.Options getBoundsOptionsForImage(Uri uri, Context context)
+ throws IOException {
+ InputStream input = context.getContentResolver().openInputStream(uri);
+ BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
+ onlyBoundsOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
+ input.close();
+ return onlyBoundsOptions;
+ }
+
+ private static int getPowerOfTwoForSampleRatio(double ratio) {
+ int k = Integer.highestOneBit((int) Math.floor(ratio));
+ return Math.max(1, k);
+ }
+}
diff --git a/com/android/internal/widget/LockPatternChecker.java b/com/android/internal/widget/LockPatternChecker.java
new file mode 100644
index 0000000..85a45fd
--- /dev/null
+++ b/com/android/internal/widget/LockPatternChecker.java
@@ -0,0 +1,182 @@
+package com.android.internal.widget;
+
+import android.os.AsyncTask;
+
+import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
+
+/**
+ * Helper class to check/verify PIN/Password/Pattern asynchronously.
+ */
+public final class LockPatternChecker {
+ /**
+ * Interface for a callback to be invoked after security check.
+ */
+ public interface OnCheckCallback {
+
+ /**
+ * Invoked as soon as possible we know that the credentials match. This will be called
+ * earlier than {@link #onChecked} but only if the credentials match.
+ */
+ default void onEarlyMatched() {}
+
+ /**
+ * Invoked when a security check is finished.
+ *
+ * @param matched Whether the PIN/Password/Pattern matches the stored one.
+ * @param throttleTimeoutMs The amount of time in ms to wait before reattempting
+ * the call. Only non-0 if matched is false.
+ */
+ void onChecked(boolean matched, int throttleTimeoutMs);
+
+ /**
+ * Called when the underlying AsyncTask was cancelled.
+ */
+ default void onCancelled() {}
+ }
+
+ /**
+ * Interface for a callback to be invoked after security verification.
+ */
+ public interface OnVerifyCallback {
+ /**
+ * Invoked when a security verification is finished.
+ *
+ * @param attestation The attestation that the challenge was verified, or null.
+ * @param throttleTimeoutMs The amount of time in ms to wait before reattempting
+ * the call. Only non-0 if attestation is null.
+ */
+ void onVerified(byte[] attestation, int throttleTimeoutMs);
+ }
+
+ /**
+ * Verify a lockscreen credential asynchronously.
+ *
+ * @param utils The LockPatternUtils instance to use.
+ * @param credential The credential to check.
+ * @param challenge The challenge to verify against the credential.
+ * @param userId The user to check against the credential.
+ * @param callback The callback to be invoked with the verification result.
+ */
+ public static AsyncTask<?, ?, ?> verifyCredential(final LockPatternUtils utils,
+ final LockscreenCredential credential,
+ final long challenge,
+ final int userId,
+ final OnVerifyCallback callback) {
+ // Create a copy of the credential since checking credential is asynchrounous.
+ final LockscreenCredential credentialCopy = credential.duplicate();
+ AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
+ private int mThrottleTimeout;
+
+ @Override
+ protected byte[] doInBackground(Void... args) {
+ try {
+ return utils.verifyCredential(credentialCopy, challenge, userId);
+ } catch (RequestThrottledException ex) {
+ mThrottleTimeout = ex.getTimeoutMs();
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(byte[] result) {
+ callback.onVerified(result, mThrottleTimeout);
+ credentialCopy.zeroize();
+ }
+
+ @Override
+ protected void onCancelled() {
+ credentialCopy.zeroize();
+ }
+ };
+ task.execute();
+ return task;
+ }
+
+ /**
+ * Checks a lockscreen credential asynchronously.
+ *
+ * @param utils The LockPatternUtils instance to use.
+ * @param credential The credential to check.
+ * @param userId The user to check against the credential.
+ * @param callback The callback to be invoked with the check result.
+ */
+ public static AsyncTask<?, ?, ?> checkCredential(final LockPatternUtils utils,
+ final LockscreenCredential credential,
+ final int userId,
+ final OnCheckCallback callback) {
+ // Create a copy of the credential since checking credential is asynchrounous.
+ final LockscreenCredential credentialCopy = credential.duplicate();
+ AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
+ private int mThrottleTimeout;
+
+ @Override
+ protected Boolean doInBackground(Void... args) {
+ try {
+ return utils.checkCredential(credentialCopy, userId, callback::onEarlyMatched);
+ } catch (RequestThrottledException ex) {
+ mThrottleTimeout = ex.getTimeoutMs();
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ callback.onChecked(result, mThrottleTimeout);
+ credentialCopy.zeroize();
+ }
+
+ @Override
+ protected void onCancelled() {
+ callback.onCancelled();
+ credentialCopy.zeroize();
+ }
+ };
+ task.execute();
+ return task;
+ }
+
+ /**
+ * Perform a lockscreen credential verification explicitly on a managed profile with unified
+ * challenge, using the parent user's credential.
+ *
+ * @param utils The LockPatternUtils instance to use.
+ * @param credential The credential to check.
+ * @param challenge The challenge to verify against the credential.
+ * @param userId The user to check against the credential.
+ * @param callback The callback to be invoked with the verification result.
+ */
+ public static AsyncTask<?, ?, ?> verifyTiedProfileChallenge(final LockPatternUtils utils,
+ final LockscreenCredential credential,
+ final long challenge,
+ final int userId,
+ final OnVerifyCallback callback) {
+ // Create a copy of the credential since checking credential is asynchrounous.
+ final LockscreenCredential credentialCopy = credential.duplicate();
+ AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
+ private int mThrottleTimeout;
+
+ @Override
+ protected byte[] doInBackground(Void... args) {
+ try {
+ return utils.verifyTiedProfileChallenge(credentialCopy, challenge, userId);
+ } catch (RequestThrottledException ex) {
+ mThrottleTimeout = ex.getTimeoutMs();
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(byte[] result) {
+ callback.onVerified(result, mThrottleTimeout);
+ credentialCopy.zeroize();
+ }
+
+ @Override
+ protected void onCancelled() {
+ credentialCopy.zeroize();
+ }
+ };
+ task.execute();
+ return task;
+ }
+}
diff --git a/com/android/internal/widget/LockPatternUtils.java b/com/android/internal/widget/LockPatternUtils.java
new file mode 100644
index 0000000..d9b2902
--- /dev/null
+++ b/com/android/internal/widget/LockPatternUtils.java
@@ -0,0 +1,1857 @@
+/*
+ * Copyright (C) 2007 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.internal.widget;
+
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.PasswordMetrics;
+import android.app.trust.IStrongAuthTracker;
+import android.app.trust.TrustManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.IStorageManager;
+import android.os.storage.StorageManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.util.SparseLongArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+
+import com.google.android.collect.Lists;
+
+import libcore.util.HexEncoding;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * Utilities for the lock pattern and its settings.
+ */
+public class LockPatternUtils {
+ private static final String TAG = "LockPatternUtils";
+ private static final boolean FRP_CREDENTIAL_ENABLED = true;
+
+ /**
+ * The key to identify when the lock pattern enabled flag is being accessed for legacy reasons.
+ */
+ public static final String LEGACY_LOCK_PATTERN_ENABLED = "legacy_lock_pattern_enabled";
+
+ /**
+ * The interval of the countdown for showing progress of the lockout.
+ */
+ public static final long FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS = 1000L;
+
+ /**
+ * This dictates when we start telling the user that continued failed attempts will wipe
+ * their device.
+ */
+ public static final int FAILED_ATTEMPTS_BEFORE_WIPE_GRACE = 5;
+
+ /**
+ * The minimum number of dots in a valid pattern.
+ */
+ public static final int MIN_LOCK_PATTERN_SIZE = 4;
+
+ /**
+ * The minimum size of a valid password.
+ */
+ public static final int MIN_LOCK_PASSWORD_SIZE = 4;
+
+ /**
+ * The minimum number of dots the user must include in a wrong pattern attempt for it to be
+ * counted.
+ */
+ public static final int MIN_PATTERN_REGISTER_FAIL = MIN_LOCK_PATTERN_SIZE;
+
+ // NOTE: When modifying this, make sure credential sufficiency validation logic is intact.
+ public static final int CREDENTIAL_TYPE_NONE = -1;
+ public static final int CREDENTIAL_TYPE_PATTERN = 1;
+ // This is the legacy value persisted on disk. Never return it to clients, but internally
+ // we still need it to handle upgrade cases.
+ public static final int CREDENTIAL_TYPE_PASSWORD_OR_PIN = 2;
+ public static final int CREDENTIAL_TYPE_PIN = 3;
+ public static final int CREDENTIAL_TYPE_PASSWORD = 4;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"CREDENTIAL_TYPE_"}, value = {
+ CREDENTIAL_TYPE_NONE,
+ CREDENTIAL_TYPE_PATTERN,
+ CREDENTIAL_TYPE_PASSWORD,
+ CREDENTIAL_TYPE_PIN,
+ // CREDENTIAL_TYPE_PASSWORD_OR_PIN is missing on purpose.
+ })
+ public @interface CredentialType {}
+
+ /**
+ * Special user id for triggering the FRP verification flow.
+ */
+ public static final int USER_FRP = UserHandle.USER_NULL + 1;
+
+ @Deprecated
+ public final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently";
+ public final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen";
+ public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type";
+ @Deprecated
+ public final static String PASSWORD_TYPE_ALTERNATE_KEY = "lockscreen.password_type_alternate";
+ public final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt";
+ public final static String DISABLE_LOCKSCREEN_KEY = "lockscreen.disabled";
+ public final static String LOCKSCREEN_OPTIONS = "lockscreen.options";
+ @Deprecated
+ public final static String LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK
+ = "lockscreen.biometric_weak_fallback";
+ @Deprecated
+ public final static String BIOMETRIC_WEAK_EVER_CHOSEN_KEY
+ = "lockscreen.biometricweakeverchosen";
+ public final static String LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS
+ = "lockscreen.power_button_instantly_locks";
+ @Deprecated
+ public final static String LOCKSCREEN_WIDGETS_ENABLED = "lockscreen.widgets_enabled";
+
+ public final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory";
+
+ private static final String LOCK_SCREEN_OWNER_INFO = Settings.Secure.LOCK_SCREEN_OWNER_INFO;
+ private static final String LOCK_SCREEN_OWNER_INFO_ENABLED =
+ Settings.Secure.LOCK_SCREEN_OWNER_INFO_ENABLED;
+
+ private static final String LOCK_SCREEN_DEVICE_OWNER_INFO = "lockscreen.device_owner_info";
+
+ private static final String ENABLED_TRUST_AGENTS = "lockscreen.enabledtrustagents";
+ private static final String IS_TRUST_USUALLY_MANAGED = "lockscreen.istrustusuallymanaged";
+
+ public static final String PROFILE_KEY_NAME_ENCRYPT = "profile_key_name_encrypt_";
+ public static final String PROFILE_KEY_NAME_DECRYPT = "profile_key_name_decrypt_";
+ public static final String SYNTHETIC_PASSWORD_KEY_PREFIX = "synthetic_password_";
+
+ public static final String SYNTHETIC_PASSWORD_HANDLE_KEY = "sp-handle";
+ public static final String SYNTHETIC_PASSWORD_ENABLED_KEY = "enable-sp";
+ public static final int SYNTHETIC_PASSWORD_ENABLED_BY_DEFAULT = 1;
+ private static final String HISTORY_DELIMITER = ",";
+
+ @UnsupportedAppUsage
+ private final Context mContext;
+ @UnsupportedAppUsage
+ private final ContentResolver mContentResolver;
+ private DevicePolicyManager mDevicePolicyManager;
+ private ILockSettings mLockSettingsService;
+ private UserManager mUserManager;
+ private final Handler mHandler;
+ private final SparseLongArray mLockoutDeadlines = new SparseLongArray();
+ private Boolean mHasSecureLockScreen;
+
+ /**
+ * Use {@link TrustManager#isTrustUsuallyManaged(int)}.
+ *
+ * This returns the lazily-peristed value and should only be used by TrustManagerService.
+ */
+ public boolean isTrustUsuallyManaged(int userId) {
+ if (!(mLockSettingsService instanceof ILockSettings.Stub)) {
+ throw new IllegalStateException("May only be called by TrustManagerService. "
+ + "Use TrustManager.isTrustUsuallyManaged()");
+ }
+ try {
+ return getLockSettings().getBoolean(IS_TRUST_USUALLY_MANAGED, false, userId);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ public void setTrustUsuallyManaged(boolean managed, int userId) {
+ try {
+ getLockSettings().setBoolean(IS_TRUST_USUALLY_MANAGED, managed, userId);
+ } catch (RemoteException e) {
+ // System dead.
+ }
+ }
+
+ public void userPresent(int userId) {
+ try {
+ getLockSettings().userPresent(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public static final class RequestThrottledException extends Exception {
+ private int mTimeoutMs;
+ @UnsupportedAppUsage
+ public RequestThrottledException(int timeoutMs) {
+ mTimeoutMs = timeoutMs;
+ }
+
+ /**
+ * @return The amount of time in ms before another request may
+ * be executed
+ */
+ @UnsupportedAppUsage
+ public int getTimeoutMs() {
+ return mTimeoutMs;
+ }
+
+ }
+
+ @UnsupportedAppUsage
+ public DevicePolicyManager getDevicePolicyManager() {
+ if (mDevicePolicyManager == null) {
+ mDevicePolicyManager =
+ (DevicePolicyManager)mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ if (mDevicePolicyManager == null) {
+ Log.e(TAG, "Can't get DevicePolicyManagerService: is it running?",
+ new IllegalStateException("Stack trace:"));
+ }
+ }
+ return mDevicePolicyManager;
+ }
+
+ private UserManager getUserManager() {
+ if (mUserManager == null) {
+ mUserManager = UserManager.get(mContext);
+ }
+ return mUserManager;
+ }
+
+ private TrustManager getTrustManager() {
+ TrustManager trust = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
+ if (trust == null) {
+ Log.e(TAG, "Can't get TrustManagerService: is it running?",
+ new IllegalStateException("Stack trace:"));
+ }
+ return trust;
+ }
+
+ @UnsupportedAppUsage
+ public LockPatternUtils(Context context) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+
+ Looper looper = Looper.myLooper();
+ mHandler = looper != null ? new Handler(looper) : null;
+ }
+
+ @UnsupportedAppUsage
+ @VisibleForTesting
+ public ILockSettings getLockSettings() {
+ if (mLockSettingsService == null) {
+ ILockSettings service = ILockSettings.Stub.asInterface(
+ ServiceManager.getService("lock_settings"));
+ mLockSettingsService = service;
+ }
+ return mLockSettingsService;
+ }
+
+ public int getRequestedMinimumPasswordLength(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumLength(null, userId);
+ }
+
+ public int getMaximumPasswordLength(int quality) {
+ return getDevicePolicyManager().getPasswordMaximumLength(quality);
+ }
+
+ public PasswordMetrics getRequestedPasswordMetrics(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumMetrics(userId);
+ }
+
+ public int getRequestedPasswordQuality(int userId) {
+ return getDevicePolicyManager().getPasswordQuality(null, userId);
+ }
+
+ private int getRequestedPasswordHistoryLength(int userId) {
+ return getDevicePolicyManager().getPasswordHistoryLength(null, userId);
+ }
+
+ public int getRequestedPasswordMinimumLetters(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumLetters(null, userId);
+ }
+
+ public int getRequestedPasswordMinimumUpperCase(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumUpperCase(null, userId);
+ }
+
+ public int getRequestedPasswordMinimumLowerCase(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumLowerCase(null, userId);
+ }
+
+ public int getRequestedPasswordMinimumNumeric(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumNumeric(null, userId);
+ }
+
+ public int getRequestedPasswordMinimumSymbols(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumSymbols(null, userId);
+ }
+
+ public int getRequestedPasswordMinimumNonLetter(int userId) {
+ return getDevicePolicyManager().getPasswordMinimumNonLetter(null, userId);
+ }
+
+ @UnsupportedAppUsage
+ public void reportFailedPasswordAttempt(int userId) {
+ if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+ return;
+ }
+ getDevicePolicyManager().reportFailedPasswordAttempt(userId);
+ getTrustManager().reportUnlockAttempt(false /* authenticated */, userId);
+ }
+
+ @UnsupportedAppUsage
+ public void reportSuccessfulPasswordAttempt(int userId) {
+ if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+ return;
+ }
+ getDevicePolicyManager().reportSuccessfulPasswordAttempt(userId);
+ getTrustManager().reportUnlockAttempt(true /* authenticated */, userId);
+ }
+
+ public void reportPasswordLockout(int timeoutMs, int userId) {
+ if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+ return;
+ }
+ getTrustManager().reportUnlockLockout(timeoutMs, userId);
+ }
+
+ public int getCurrentFailedPasswordAttempts(int userId) {
+ if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+ return 0;
+ }
+ return getDevicePolicyManager().getCurrentFailedPasswordAttempts(userId);
+ }
+
+ public int getMaximumFailedPasswordsForWipe(int userId) {
+ if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+ return 0;
+ }
+ return getDevicePolicyManager().getMaximumFailedPasswordsForWipe(
+ null /* componentName */, userId);
+ }
+
+ /**
+ * Check to see if a credential matches the saved one.
+ * If credential matches, return an opaque attestation that the challenge was verified.
+ *
+ * @param credential The credential to check.
+ * @param challenge The challenge to verify against the credential
+ * @param userId The user whose credential is being verified
+ * @return the attestation that the challenge was verified, or null
+ * @throws RequestThrottledException if credential verification is being throttled due to
+ * to many incorrect attempts.
+ * @throws IllegalStateException if called on the main thread.
+ */
+ public byte[] verifyCredential(@NonNull LockscreenCredential credential, long challenge,
+ int userId) throws RequestThrottledException {
+ throwIfCalledOnMainThread();
+ try {
+ VerifyCredentialResponse response = getLockSettings().verifyCredential(
+ credential, challenge, userId);
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ return response.getPayload();
+ } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+ throw new RequestThrottledException(response.getTimeout());
+ } else {
+ return null;
+ }
+ } catch (RemoteException re) {
+ Log.e(TAG, "failed to verify credential", re);
+ return null;
+ }
+ }
+
+ /**
+ * Check to see if a credential matches the saved one.
+ *
+ * @param credential The credential to check.
+ * @param userId The user whose credential is being checked
+ * @param progressCallback callback to deliver early signal that the credential matches
+ * @return {@code true} if credential matches, {@code false} otherwise
+ * @throws RequestThrottledException if credential verification is being throttled due to
+ * to many incorrect attempts.
+ * @throws IllegalStateException if called on the main thread.
+ */
+ public boolean checkCredential(@NonNull LockscreenCredential credential, int userId,
+ @Nullable CheckCredentialProgressCallback progressCallback)
+ throws RequestThrottledException {
+ throwIfCalledOnMainThread();
+ try {
+ VerifyCredentialResponse response = getLockSettings().checkCredential(
+ credential, userId, wrapCallback(progressCallback));
+
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ return true;
+ } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+ throw new RequestThrottledException(response.getTimeout());
+ } else {
+ return false;
+ }
+ } catch (RemoteException re) {
+ Log.e(TAG, "failed to check credential", re);
+ return false;
+ }
+ }
+
+ /**
+ * Check if the credential of a managed profile with unified challenge matches. In this context,
+ * The credential should be the parent user's lockscreen password. If credential matches,
+ * return an opaque attestation associated with the managed profile that the challenge was
+ * verified.
+ *
+ * @param credential The parent user's credential to check.
+ * @param challenge The challenge to verify against the credential
+ * @return the attestation that the challenge was verified, or null
+ * @param userId The managed profile user id
+ * @throws RequestThrottledException if credential verification is being throttled due to
+ * to many incorrect attempts.
+ * @throws IllegalStateException if called on the main thread.
+ */
+ public byte[] verifyTiedProfileChallenge(@NonNull LockscreenCredential credential,
+ long challenge, int userId) throws RequestThrottledException {
+ throwIfCalledOnMainThread();
+ try {
+ VerifyCredentialResponse response =
+ getLockSettings().verifyTiedProfileChallenge(credential, challenge, userId);
+
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ return response.getPayload();
+ } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+ throw new RequestThrottledException(response.getTimeout());
+ } else {
+ return null;
+ }
+ } catch (RemoteException re) {
+ Log.e(TAG, "failed to verify tied profile credential", re);
+ return null;
+ }
+ }
+
+ /**
+ * Check to see if vold already has the password.
+ * Note that this also clears vold's copy of the password.
+ * @return Whether the vold password matches or not.
+ */
+ public boolean checkVoldPassword(int userId) {
+ try {
+ return getLockSettings().checkVoldPassword(userId);
+ } catch (RemoteException re) {
+ Log.e(TAG, "failed to check vold password", re);
+ return false;
+ }
+ }
+
+ /**
+ * Returns the password history hash factor, needed to check new password against password
+ * history with {@link #checkPasswordHistory(byte[], byte[], int)}
+ */
+ public byte[] getPasswordHistoryHashFactor(@NonNull LockscreenCredential currentPassword,
+ int userId) {
+ try {
+ return getLockSettings().getHashFactor(currentPassword, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get hash factor", e);
+ return null;
+ }
+ }
+
+ /**
+ * Check to see if a password matches any of the passwords stored in the
+ * password history.
+ *
+ * @param passwordToCheck The password to check.
+ * @param hashFactor Hash factor of the current user returned from
+ * {@link ILockSettings#getHashFactor}
+ * @return Whether the password matches any in the history.
+ */
+ public boolean checkPasswordHistory(byte[] passwordToCheck, byte[] hashFactor, int userId) {
+ if (passwordToCheck == null || passwordToCheck.length == 0) {
+ Log.e(TAG, "checkPasswordHistory: empty password");
+ return false;
+ }
+ String passwordHistory = getString(PASSWORD_HISTORY_KEY, userId);
+ if (TextUtils.isEmpty(passwordHistory)) {
+ return false;
+ }
+ int passwordHistoryLength = getRequestedPasswordHistoryLength(userId);
+ if(passwordHistoryLength == 0) {
+ return false;
+ }
+ String legacyHash = legacyPasswordToHash(passwordToCheck, userId);
+ String passwordHash = passwordToHistoryHash(passwordToCheck, hashFactor, userId);
+ String[] history = passwordHistory.split(HISTORY_DELIMITER);
+ // Password History may be too long...
+ for (int i = 0; i < Math.min(passwordHistoryLength, history.length); i++) {
+ if (history[i].equals(legacyHash) || history[i].equals(passwordHash)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if the user has ever chosen a pattern. This is true even if the pattern is
+ * currently cleared.
+ *
+ * @return True if the user has ever chosen a pattern.
+ */
+ public boolean isPatternEverChosen(int userId) {
+ return getBoolean(PATTERN_EVER_CHOSEN_KEY, false, userId);
+ }
+
+ /**
+ * Records that the user has chosen a pattern at some time, even if the pattern is
+ * currently cleared.
+ */
+ public void reportPatternWasChosen(int userId) {
+ setBoolean(PATTERN_EVER_CHOSEN_KEY, true, userId);
+ }
+
+ /**
+ * Used by device policy manager to validate the current password
+ * information it has.
+ * @Deprecated use {@link #getKeyguardStoredPasswordQuality}
+ */
+ @UnsupportedAppUsage
+ public int getActivePasswordQuality(int userId) {
+ return getKeyguardStoredPasswordQuality(userId);
+ }
+
+ /**
+ * Use it to reset keystore without wiping work profile
+ */
+ public void resetKeyStore(int userId) {
+ try {
+ getLockSettings().resetKeyStore(userId);
+ } catch (RemoteException e) {
+ // It should not happen
+ Log.e(TAG, "Couldn't reset keystore " + e);
+ }
+ }
+
+ /**
+ * Disable showing lock screen at all for a given user.
+ * This is only meaningful if pattern, pin or password are not set.
+ *
+ * @param disable Disables lock screen when true
+ * @param userId User ID of the user this has effect on
+ */
+ public void setLockScreenDisabled(boolean disable, int userId) {
+ setBoolean(DISABLE_LOCKSCREEN_KEY, disable, userId);
+ }
+
+ /**
+ * Determine if LockScreen is disabled for the current user. This is used to decide whether
+ * LockScreen is shown after reboot or after screen timeout / short press on power.
+ *
+ * @return true if lock screen is disabled
+ */
+ @UnsupportedAppUsage
+ public boolean isLockScreenDisabled(int userId) {
+ if (isSecure(userId)) {
+ return false;
+ }
+ boolean disabledByDefault = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_disableLockscreenByDefault);
+ boolean isSystemUser = UserManager.isSplitSystemUser() && userId == UserHandle.USER_SYSTEM;
+ UserInfo userInfo = getUserManager().getUserInfo(userId);
+ boolean isDemoUser = UserManager.isDeviceInDemoMode(mContext) && userInfo != null
+ && userInfo.isDemo();
+ return getBoolean(DISABLE_LOCKSCREEN_KEY, false, userId)
+ || (disabledByDefault && !isSystemUser)
+ || isDemoUser;
+ }
+
+ /** Returns if the given quality maps to an alphabetic password */
+ public static boolean isQualityAlphabeticPassword(int quality) {
+ return quality >= PASSWORD_QUALITY_ALPHABETIC;
+ }
+
+ /** Returns if the given quality maps to an numeric pin */
+ public static boolean isQualityNumericPin(int quality) {
+ return quality == PASSWORD_QUALITY_NUMERIC || quality == PASSWORD_QUALITY_NUMERIC_COMPLEX;
+ }
+
+ /** Returns the canonical password quality corresponding to the given credential type. */
+ public static int credentialTypeToPasswordQuality(int credentialType) {
+ switch (credentialType) {
+ case CREDENTIAL_TYPE_NONE:
+ return PASSWORD_QUALITY_UNSPECIFIED;
+ case CREDENTIAL_TYPE_PATTERN:
+ return PASSWORD_QUALITY_SOMETHING;
+ case CREDENTIAL_TYPE_PIN:
+ return PASSWORD_QUALITY_NUMERIC;
+ case CREDENTIAL_TYPE_PASSWORD:
+ return PASSWORD_QUALITY_ALPHABETIC;
+ default:
+ throw new IllegalStateException("Unknown type: " + credentialType);
+ }
+ }
+
+ /**
+ * Save a new lockscreen credential.
+ *
+ * <p> This method will fail (returning {@code false}) if the previously saved credential
+ * provided is incorrect, or if the lockscreen verification is still being throttled.
+ *
+ * @param newCredential The new credential to save
+ * @param savedCredential The current credential
+ * @param userHandle the user whose lockscreen credential is to be changed
+ *
+ * @return whether this method saved the new password successfully or not. This flow will fail
+ * and return false if the given credential is wrong.
+ * @throws RuntimeException if password change encountered an unrecoverable error.
+ * @throws UnsupportedOperationException secure lockscreen is not supported on this device.
+ * @throws IllegalArgumentException if new credential is too short.
+ */
+ public boolean setLockCredential(@NonNull LockscreenCredential newCredential,
+ @NonNull LockscreenCredential savedCredential, int userHandle) {
+ if (!hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires the lock screen feature.");
+ }
+ newCredential.checkLength();
+
+ try {
+ if (!getLockSettings().setLockCredential(newCredential, savedCredential, userHandle)) {
+ return false;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to save lock password", e);
+ }
+
+ onPostPasswordChanged(newCredential, userHandle);
+ return true;
+ }
+
+ private void onPostPasswordChanged(LockscreenCredential newCredential, int userHandle) {
+ updateEncryptionPasswordIfNeeded(newCredential, userHandle);
+ if (newCredential.isPattern()) {
+ reportPatternWasChosen(userHandle);
+ }
+ updatePasswordHistory(newCredential, userHandle);
+ reportEnabledTrustAgentsChanged(userHandle);
+ }
+
+ private void updateCryptoUserInfo(int userId) {
+ if (userId != UserHandle.USER_SYSTEM) {
+ return;
+ }
+
+ final String ownerInfo = isOwnerInfoEnabled(userId) ? getOwnerInfo(userId) : "";
+
+ IBinder service = ServiceManager.getService("mount");
+ if (service == null) {
+ Log.e(TAG, "Could not find the mount service to update the user info");
+ return;
+ }
+
+ IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+ try {
+ Log.d(TAG, "Setting owner info");
+ storageManager.setField(StorageManager.OWNER_INFO_KEY, ownerInfo);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error changing user info", e);
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void setOwnerInfo(String info, int userId) {
+ setString(LOCK_SCREEN_OWNER_INFO, info, userId);
+ updateCryptoUserInfo(userId);
+ }
+
+ @UnsupportedAppUsage
+ public void setOwnerInfoEnabled(boolean enabled, int userId) {
+ setBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, enabled, userId);
+ updateCryptoUserInfo(userId);
+ }
+
+ @UnsupportedAppUsage
+ public String getOwnerInfo(int userId) {
+ return getString(LOCK_SCREEN_OWNER_INFO, userId);
+ }
+
+ public boolean isOwnerInfoEnabled(int userId) {
+ return getBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, false, userId);
+ }
+
+ /**
+ * Sets the device owner information. If the information is {@code null} or empty then the
+ * device owner info is cleared.
+ *
+ * @param info Device owner information which will be displayed instead of the user
+ * owner info.
+ */
+ public void setDeviceOwnerInfo(String info) {
+ if (info != null && info.isEmpty()) {
+ info = null;
+ }
+
+ setString(LOCK_SCREEN_DEVICE_OWNER_INFO, info, UserHandle.USER_SYSTEM);
+ }
+
+ public String getDeviceOwnerInfo() {
+ return getString(LOCK_SCREEN_DEVICE_OWNER_INFO, UserHandle.USER_SYSTEM);
+ }
+
+ public boolean isDeviceOwnerInfoEnabled() {
+ return getDeviceOwnerInfo() != null;
+ }
+
+ /** Update the encryption password if it is enabled **/
+ private void updateEncryptionPassword(final int type, final byte[] password) {
+ if (!hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires the lock screen feature.");
+ }
+ if (!isDeviceEncryptionEnabled()) {
+ return;
+ }
+ final IBinder service = ServiceManager.getService("mount");
+ if (service == null) {
+ Log.e(TAG, "Could not find the mount service to update the encryption password");
+ return;
+ }
+
+ // TODO(b/120484642): This is a location where we still use a String for vold
+ String passwordString = password != null ? new String(password) : null;
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... dummy) {
+ IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+ try {
+ storageManager.changeEncryptionPassword(type, passwordString);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error changing encryption password", e);
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ /**
+ * Update device encryption password if calling user is USER_SYSTEM and device supports
+ * encryption.
+ */
+ private void updateEncryptionPasswordIfNeeded(LockscreenCredential credential, int userHandle) {
+ // Update the device encryption password.
+ if (userHandle != UserHandle.USER_SYSTEM || !isDeviceEncryptionEnabled()) {
+ return;
+ }
+ if (!shouldEncryptWithCredentials(true)) {
+ updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null);
+ return;
+ }
+ if (credential.isNone()) {
+ // Set the encryption password to default.
+ setCredentialRequiredToDecrypt(false);
+ }
+ updateEncryptionPassword(credential.getStorageCryptType(), credential.getCredential());
+ }
+
+ /**
+ * Store the hash of the *current* password in the password history list, if device policy
+ * enforces password history requirement.
+ */
+ private void updatePasswordHistory(LockscreenCredential password, int userHandle) {
+ if (password.isNone()) {
+ return;
+ }
+ if (password.isPattern()) {
+ // Do not keep track of historical patterns
+ return;
+ }
+ // Add the password to the password history. We assume all
+ // password hashes have the same length for simplicity of implementation.
+ String passwordHistory = getString(PASSWORD_HISTORY_KEY, userHandle);
+ if (passwordHistory == null) {
+ passwordHistory = "";
+ }
+ int passwordHistoryLength = getRequestedPasswordHistoryLength(userHandle);
+ if (passwordHistoryLength == 0) {
+ passwordHistory = "";
+ } else {
+ final byte[] hashFactor = getPasswordHistoryHashFactor(password, userHandle);
+ String hash = passwordToHistoryHash(password.getCredential(), hashFactor, userHandle);
+ if (hash == null) {
+ Log.e(TAG, "Compute new style password hash failed, fallback to legacy style");
+ hash = legacyPasswordToHash(password.getCredential(), userHandle);
+ }
+ if (TextUtils.isEmpty(passwordHistory)) {
+ passwordHistory = hash;
+ } else {
+ String[] history = passwordHistory.split(HISTORY_DELIMITER);
+ StringJoiner joiner = new StringJoiner(HISTORY_DELIMITER);
+ joiner.add(hash);
+ for (int i = 0; i < passwordHistoryLength - 1 && i < history.length; i++) {
+ joiner.add(history[i]);
+ }
+ passwordHistory = joiner.toString();
+ }
+ }
+ setString(PASSWORD_HISTORY_KEY, passwordHistory, userHandle);
+ }
+
+ /**
+ * Determine if the device supports encryption, even if it's set to default. This
+ * differs from isDeviceEncrypted() in that it returns true even if the device is
+ * encrypted with the default password.
+ * @return true if device encryption is enabled
+ */
+ @UnsupportedAppUsage
+ public static boolean isDeviceEncryptionEnabled() {
+ return StorageManager.isEncrypted();
+ }
+
+ /**
+ * Determine if the device is file encrypted
+ * @return true if device is file encrypted
+ */
+ public static boolean isFileEncryptionEnabled() {
+ return StorageManager.isFileEncryptedNativeOrEmulated();
+ }
+
+ /**
+ * Clears the encryption password.
+ */
+ public void clearEncryptionPassword() {
+ updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, null);
+ }
+
+ /**
+ * Retrieves the quality mode for {@code userHandle}.
+ * @see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)
+ *
+ * @return stored password quality
+ * @deprecated use {@link #getCredentialTypeForUser(int)} instead
+ */
+ @UnsupportedAppUsage
+ @Deprecated
+ public int getKeyguardStoredPasswordQuality(int userHandle) {
+ return credentialTypeToPasswordQuality(getCredentialTypeForUser(userHandle));
+ }
+
+ /**
+ * Enables/disables the Separate Profile Challenge for this {@code userHandle}. This is a no-op
+ * for user handles that do not belong to a managed profile.
+ *
+ * @param userHandle Managed profile user id
+ * @param enabled True if separate challenge is enabled
+ * @param profilePassword Managed profile previous password. Null when {@code enabled} is
+ * true
+ */
+ public void setSeparateProfileChallengeEnabled(int userHandle, boolean enabled,
+ LockscreenCredential profilePassword) {
+ if (!isManagedProfile(userHandle)) {
+ return;
+ }
+ try {
+ getLockSettings().setSeparateProfileChallengeEnabled(userHandle, enabled,
+ profilePassword);
+ reportEnabledTrustAgentsChanged(userHandle);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't update work profile challenge enabled");
+ }
+ }
+
+ /**
+ * Returns true if {@code userHandle} is a managed profile with separate challenge.
+ */
+ public boolean isSeparateProfileChallengeEnabled(int userHandle) {
+ return isManagedProfile(userHandle) && hasSeparateChallenge(userHandle);
+ }
+
+ /**
+ * Returns true if {@code userHandle} is a managed profile with unified challenge.
+ */
+ public boolean isManagedProfileWithUnifiedChallenge(int userHandle) {
+ return isManagedProfile(userHandle) && !hasSeparateChallenge(userHandle);
+ }
+
+ /**
+ * Retrieves whether the current DPM allows use of the Profile Challenge.
+ */
+ public boolean isSeparateProfileChallengeAllowed(int userHandle) {
+ return isManagedProfile(userHandle)
+ && getDevicePolicyManager().isSeparateProfileChallengeAllowed(userHandle);
+ }
+
+ /**
+ * Retrieves whether the current profile and device locks can be unified.
+ * @param userHandle profile user handle.
+ */
+ public boolean isSeparateProfileChallengeAllowedToUnify(int userHandle) {
+ return getDevicePolicyManager().isProfileActivePasswordSufficientForParent(userHandle)
+ && !getUserManager().hasUserRestriction(
+ UserManager.DISALLOW_UNIFIED_PASSWORD, UserHandle.of(userHandle));
+ }
+
+ private boolean hasSeparateChallenge(int userHandle) {
+ try {
+ return getLockSettings().getSeparateProfileChallengeEnabled(userHandle);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't get separate profile challenge enabled");
+ // Default value is false
+ return false;
+ }
+ }
+
+ private boolean isManagedProfile(int userHandle) {
+ final UserInfo info = getUserManager().getUserInfo(userHandle);
+ return info != null && info.isManagedProfile();
+ }
+
+ /**
+ * Deserialize a pattern.
+ * @param bytes The pattern serialized with {@link #patternToByteArray}
+ * @return The pattern.
+ */
+ public static List<LockPatternView.Cell> byteArrayToPattern(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+
+ List<LockPatternView.Cell> result = Lists.newArrayList();
+
+ for (int i = 0; i < bytes.length; i++) {
+ byte b = (byte) (bytes[i] - '1');
+ result.add(LockPatternView.Cell.of(b / 3, b % 3));
+ }
+ return result;
+ }
+
+ /**
+ * Serialize a pattern.
+ * @param pattern The pattern.
+ * @return The pattern in byte array form.
+ */
+ public static byte[] patternToByteArray(List<LockPatternView.Cell> pattern) {
+ if (pattern == null) {
+ return new byte[0];
+ }
+ final int patternSize = pattern.size();
+
+ byte[] res = new byte[patternSize];
+ for (int i = 0; i < patternSize; i++) {
+ LockPatternView.Cell cell = pattern.get(i);
+ res[i] = (byte) (cell.getRow() * 3 + cell.getColumn() + '1');
+ }
+ return res;
+ }
+
+ private String getSalt(int userId) {
+ long salt = getLong(LOCK_PASSWORD_SALT_KEY, 0, userId);
+ if (salt == 0) {
+ try {
+ salt = SecureRandom.getInstance("SHA1PRNG").nextLong();
+ setLong(LOCK_PASSWORD_SALT_KEY, salt, userId);
+ Log.v(TAG, "Initialized lock password salt for user: " + userId);
+ } catch (NoSuchAlgorithmException e) {
+ // Throw an exception rather than storing a password we'll never be able to recover
+ throw new IllegalStateException("Couldn't get SecureRandom number", e);
+ }
+ }
+ return Long.toHexString(salt);
+ }
+
+ /**
+ * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash.
+ * Not the most secure, but it is at least a second level of protection. First level is that
+ * the file is in a location only readable by the system process.
+ *
+ * @param password the gesture pattern.
+ *
+ * @return the hash of the pattern in a byte array.
+ * TODO: move to LockscreenCredential class
+ */
+ public String legacyPasswordToHash(byte[] password, int userId) {
+ if (password == null || password.length == 0) {
+ return null;
+ }
+
+ try {
+ // Previously the password was passed as a String with the following code:
+ // byte[] saltedPassword = (password + getSalt(userId)).getBytes();
+ // The code below creates the identical digest preimage using byte arrays:
+ byte[] salt = getSalt(userId).getBytes();
+ byte[] saltedPassword = Arrays.copyOf(password, password.length + salt.length);
+ System.arraycopy(salt, 0, saltedPassword, password.length, salt.length);
+ byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
+ byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
+
+ byte[] combined = new byte[sha1.length + md5.length];
+ System.arraycopy(sha1, 0, combined, 0, sha1.length);
+ System.arraycopy(md5, 0, combined, sha1.length, md5.length);
+
+ final char[] hexEncoded = HexEncoding.encode(combined);
+ Arrays.fill(saltedPassword, (byte) 0);
+ return new String(hexEncoded);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError("Missing digest algorithm: ", e);
+ }
+ }
+
+ /**
+ * Hash the password for password history check purpose.
+ * TODO: move to LockscreenCredential class
+ */
+ private String passwordToHistoryHash(byte[] passwordToHash, byte[] hashFactor, int userId) {
+ if (passwordToHash == null || passwordToHash.length == 0 || hashFactor == null) {
+ return null;
+ }
+ try {
+ MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+ sha256.update(hashFactor);
+ byte[] salt = getSalt(userId).getBytes();
+ byte[] saltedPassword = Arrays.copyOf(passwordToHash, passwordToHash.length
+ + salt.length);
+ System.arraycopy(salt, 0, saltedPassword, passwordToHash.length, salt.length);
+ sha256.update(saltedPassword);
+ Arrays.fill(saltedPassword, (byte) 0);
+ return new String(HexEncoding.encode(sha256.digest()));
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError("Missing digest algorithm: ", e);
+ }
+ }
+
+ /**
+ * Returns the credential type of the user, can be one of {@link #CREDENTIAL_TYPE_NONE},
+ * {@link #CREDENTIAL_TYPE_PATTERN}, {@link #CREDENTIAL_TYPE_PIN} and
+ * {@link #CREDENTIAL_TYPE_PASSWORD}
+ */
+ public @CredentialType int getCredentialTypeForUser(int userHandle) {
+ try {
+ return getLockSettings().getCredentialType(userHandle);
+ } catch (RemoteException re) {
+ Log.e(TAG, "failed to get credential type", re);
+ return CREDENTIAL_TYPE_NONE;
+ }
+ }
+
+ /**
+ * @param userId the user for which to report the value
+ * @return Whether the lock screen is secured.
+ */
+ @UnsupportedAppUsage
+ public boolean isSecure(int userId) {
+ int type = getCredentialTypeForUser(userId);
+ return type != CREDENTIAL_TYPE_NONE;
+ }
+
+ @UnsupportedAppUsage
+ public boolean isLockPasswordEnabled(int userId) {
+ int type = getCredentialTypeForUser(userId);
+ return type == CREDENTIAL_TYPE_PASSWORD || type == CREDENTIAL_TYPE_PIN;
+ }
+
+ /**
+ * @return Whether the lock pattern is enabled
+ */
+ @UnsupportedAppUsage
+ public boolean isLockPatternEnabled(int userId) {
+ int type = getCredentialTypeForUser(userId);
+ return type == CREDENTIAL_TYPE_PATTERN;
+ }
+
+ @Deprecated
+ public boolean isLegacyLockPatternEnabled(int userId) {
+ // Note: this value should default to {@code true} to avoid any reset that might result.
+ // We must use a special key to read this value, since it will by default return the value
+ // based on the new logic.
+ return getBoolean(LEGACY_LOCK_PATTERN_ENABLED, true, userId);
+ }
+
+ @Deprecated
+ public void setLegacyLockPatternEnabled(int userId) {
+ setBoolean(Settings.Secure.LOCK_PATTERN_ENABLED, true, userId);
+ }
+
+ /**
+ * @return Whether the visible pattern is enabled.
+ */
+ @UnsupportedAppUsage
+ public boolean isVisiblePatternEnabled(int userId) {
+ return getBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, false, userId);
+ }
+
+ /**
+ * Set whether the visible pattern is enabled.
+ */
+ public void setVisiblePatternEnabled(boolean enabled, int userId) {
+ setBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, enabled, userId);
+
+ // Update for crypto if owner
+ if (userId != UserHandle.USER_SYSTEM) {
+ return;
+ }
+
+ IBinder service = ServiceManager.getService("mount");
+ if (service == null) {
+ Log.e(TAG, "Could not find the mount service to update the user info");
+ return;
+ }
+
+ IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+ try {
+ storageManager.setField(StorageManager.PATTERN_VISIBLE_KEY, enabled ? "1" : "0");
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error changing pattern visible state", e);
+ }
+ }
+
+ public boolean isVisiblePatternEverChosen(int userId) {
+ return getString(Settings.Secure.LOCK_PATTERN_VISIBLE, userId) != null;
+ }
+
+ /**
+ * Set whether the visible password is enabled for cryptkeeper screen.
+ */
+ public void setVisiblePasswordEnabled(boolean enabled, int userId) {
+ // Update for crypto if owner
+ if (userId != UserHandle.USER_SYSTEM) {
+ return;
+ }
+
+ IBinder service = ServiceManager.getService("mount");
+ if (service == null) {
+ Log.e(TAG, "Could not find the mount service to update the user info");
+ return;
+ }
+
+ IStorageManager storageManager = IStorageManager.Stub.asInterface(service);
+ try {
+ storageManager.setField(StorageManager.PASSWORD_VISIBLE_KEY, enabled ? "1" : "0");
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error changing password visible state", e);
+ }
+ }
+
+ /**
+ * @return Whether tactile feedback for the pattern is enabled.
+ */
+ @UnsupportedAppUsage
+ public boolean isTactileFeedbackEnabled() {
+ return Settings.System.getIntForUser(mContentResolver,
+ Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
+ }
+
+ /**
+ * Set and store the lockout deadline, meaning the user can't attempt his/her unlock
+ * pattern until the deadline has passed.
+ * @return the chosen deadline.
+ */
+ @UnsupportedAppUsage
+ public long setLockoutAttemptDeadline(int userId, int timeoutMs) {
+ final long deadline = SystemClock.elapsedRealtime() + timeoutMs;
+ if (userId == USER_FRP) {
+ // For secure password storage (that is required for FRP), the underlying storage also
+ // enforces the deadline. Since we cannot store settings for the FRP user, don't.
+ return deadline;
+ }
+ mLockoutDeadlines.put(userId, deadline);
+ return deadline;
+ }
+
+ /**
+ * @return The elapsed time in millis in the future when the user is allowed to
+ * attempt to enter his/her lock pattern, or 0 if the user is welcome to
+ * enter a pattern.
+ */
+ public long getLockoutAttemptDeadline(int userId) {
+ final long deadline = mLockoutDeadlines.get(userId, 0L);
+ final long now = SystemClock.elapsedRealtime();
+ if (deadline < now && deadline != 0) {
+ // timeout expired
+ mLockoutDeadlines.put(userId, 0);
+ return 0L;
+ }
+ return deadline;
+ }
+
+ private boolean getBoolean(String secureSettingKey, boolean defaultValue, int userId) {
+ try {
+ return getLockSettings().getBoolean(secureSettingKey, defaultValue, userId);
+ } catch (RemoteException re) {
+ return defaultValue;
+ }
+ }
+
+ private void setBoolean(String secureSettingKey, boolean enabled, int userId) {
+ try {
+ getLockSettings().setBoolean(secureSettingKey, enabled, userId);
+ } catch (RemoteException re) {
+ // What can we do?
+ Log.e(TAG, "Couldn't write boolean " + secureSettingKey + re);
+ }
+ }
+
+ private long getLong(String secureSettingKey, long defaultValue, int userHandle) {
+ try {
+ return getLockSettings().getLong(secureSettingKey, defaultValue, userHandle);
+ } catch (RemoteException re) {
+ return defaultValue;
+ }
+ }
+
+ @UnsupportedAppUsage
+ private void setLong(String secureSettingKey, long value, int userHandle) {
+ try {
+ getLockSettings().setLong(secureSettingKey, value, userHandle);
+ } catch (RemoteException re) {
+ // What can we do?
+ Log.e(TAG, "Couldn't write long " + secureSettingKey + re);
+ }
+ }
+
+ @UnsupportedAppUsage
+ private String getString(String secureSettingKey, int userHandle) {
+ try {
+ return getLockSettings().getString(secureSettingKey, null, userHandle);
+ } catch (RemoteException re) {
+ return null;
+ }
+ }
+
+ @UnsupportedAppUsage
+ private void setString(String secureSettingKey, String value, int userHandle) {
+ try {
+ getLockSettings().setString(secureSettingKey, value, userHandle);
+ } catch (RemoteException re) {
+ // What can we do?
+ Log.e(TAG, "Couldn't write string " + secureSettingKey + re);
+ }
+ }
+
+ public void setPowerButtonInstantlyLocks(boolean enabled, int userId) {
+ setBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, enabled, userId);
+ }
+
+ @UnsupportedAppUsage
+ public boolean getPowerButtonInstantlyLocks(int userId) {
+ return getBoolean(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, true, userId);
+ }
+
+ public boolean isPowerButtonInstantlyLocksEverChosen(int userId) {
+ return getString(LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, userId) != null;
+ }
+
+ public void setEnabledTrustAgents(Collection<ComponentName> activeTrustAgents, int userId) {
+ StringBuilder sb = new StringBuilder();
+ for (ComponentName cn : activeTrustAgents) {
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ sb.append(cn.flattenToShortString());
+ }
+ setString(ENABLED_TRUST_AGENTS, sb.toString(), userId);
+ getTrustManager().reportEnabledTrustAgentsChanged(userId);
+ }
+
+ public List<ComponentName> getEnabledTrustAgents(int userId) {
+ String serialized = getString(ENABLED_TRUST_AGENTS, userId);
+ if (TextUtils.isEmpty(serialized)) {
+ return null;
+ }
+ String[] split = serialized.split(",");
+ ArrayList<ComponentName> activeTrustAgents = new ArrayList<ComponentName>(split.length);
+ for (String s : split) {
+ if (!TextUtils.isEmpty(s)) {
+ activeTrustAgents.add(ComponentName.unflattenFromString(s));
+ }
+ }
+ return activeTrustAgents;
+ }
+
+ /**
+ * Disable trust until credentials have been entered for user {@code userId}.
+ *
+ * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+ *
+ * @param userId either an explicit user id or {@link android.os.UserHandle#USER_ALL}
+ */
+ public void requireCredentialEntry(int userId) {
+ requireStrongAuth(StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId);
+ }
+
+ /**
+ * Requests strong authentication for user {@code userId}.
+ *
+ * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+ *
+ * @param strongAuthReason a combination of {@link StrongAuthTracker.StrongAuthFlags} indicating
+ * the reason for and the strength of the requested authentication.
+ * @param userId either an explicit user id or {@link android.os.UserHandle#USER_ALL}
+ */
+ public void requireStrongAuth(@StrongAuthTracker.StrongAuthFlags int strongAuthReason,
+ int userId) {
+ try {
+ getLockSettings().requireStrongAuth(strongAuthReason, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error while requesting strong auth: " + e);
+ }
+ }
+
+ private void reportEnabledTrustAgentsChanged(int userHandle) {
+ getTrustManager().reportEnabledTrustAgentsChanged(userHandle);
+ }
+
+ public boolean isCredentialRequiredToDecrypt(boolean defaultValue) {
+ final int value = Settings.Global.getInt(mContentResolver,
+ Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, -1);
+ return value == -1 ? defaultValue : (value != 0);
+ }
+
+ public void setCredentialRequiredToDecrypt(boolean required) {
+ if (!(getUserManager().isSystemUser() || getUserManager().isPrimaryUser())) {
+ throw new IllegalStateException(
+ "Only the system or primary user may call setCredentialRequiredForDecrypt()");
+ }
+
+ if (isDeviceEncryptionEnabled()){
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, required ? 1 : 0);
+ }
+ }
+
+ private boolean isDoNotAskCredentialsOnBootSet() {
+ return getDevicePolicyManager().getDoNotAskCredentialsOnBoot();
+ }
+
+ private boolean shouldEncryptWithCredentials(boolean defaultValue) {
+ return isCredentialRequiredToDecrypt(defaultValue) && !isDoNotAskCredentialsOnBootSet();
+ }
+
+ private void throwIfCalledOnMainThread() {
+ if (Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException("should not be called from the main thread.");
+ }
+ }
+
+ public void registerStrongAuthTracker(final StrongAuthTracker strongAuthTracker) {
+ try {
+ getLockSettings().registerStrongAuthTracker(strongAuthTracker.mStub);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Could not register StrongAuthTracker");
+ }
+ }
+
+ public void unregisterStrongAuthTracker(final StrongAuthTracker strongAuthTracker) {
+ try {
+ getLockSettings().unregisterStrongAuthTracker(strongAuthTracker.mStub);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not unregister StrongAuthTracker", e);
+ }
+ }
+
+ public void reportSuccessfulBiometricUnlock(boolean isStrongBiometric, int userId) {
+ try {
+ getLockSettings().reportSuccessfulBiometricUnlock(isStrongBiometric, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not report successful biometric unlock", e);
+ }
+ }
+
+ public void scheduleNonStrongBiometricIdleTimeout(int userId) {
+ try {
+ getLockSettings().scheduleNonStrongBiometricIdleTimeout(userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not schedule non-strong biometric idle timeout", e);
+ }
+ }
+
+ /**
+ * @see StrongAuthTracker#getStrongAuthForUser
+ */
+ public int getStrongAuthForUser(int userId) {
+ try {
+ return getLockSettings().getStrongAuthForUser(userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not get StrongAuth", e);
+ return StrongAuthTracker.getDefaultFlags(mContext);
+ }
+ }
+
+ /**
+ * @see StrongAuthTracker#isTrustAllowedForUser
+ */
+ public boolean isTrustAllowedForUser(int userId) {
+ return getStrongAuthForUser(userId) == StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
+ }
+
+ /**
+ * @see StrongAuthTracker#isBiometricAllowedForUser(int)
+ */
+ public boolean isBiometricAllowedForUser(int userId) {
+ return (getStrongAuthForUser(userId) & ~StrongAuthTracker.ALLOWING_BIOMETRIC) == 0;
+ }
+
+ public boolean isUserInLockdown(int userId) {
+ return getStrongAuthForUser(userId)
+ == StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
+ }
+
+ private ICheckCredentialProgressCallback wrapCallback(
+ final CheckCredentialProgressCallback callback) {
+ if (callback == null) {
+ return null;
+ } else {
+ if (mHandler == null) {
+ throw new IllegalStateException("Must construct LockPatternUtils on a looper thread"
+ + " to use progress callbacks.");
+ }
+ return new ICheckCredentialProgressCallback.Stub() {
+
+ @Override
+ public void onCredentialVerified() throws RemoteException {
+ mHandler.post(callback::onEarlyMatched);
+ }
+ };
+ }
+ }
+
+ private LockSettingsInternal getLockSettingsInternal() {
+ LockSettingsInternal service = LocalServices.getService(LockSettingsInternal.class);
+ if (service == null) {
+ throw new SecurityException("Only available to system server itself");
+ }
+ return service;
+ }
+ /**
+ * Create an escrow token for the current user, which can later be used to unlock FBE
+ * or change user password.
+ *
+ * After adding, if the user currently has lockscreen password, he will need to perform a
+ * confirm credential operation in order to activate the token for future use. If the user
+ * has no secure lockscreen, then the token is activated immediately.
+ *
+ * <p>This method is only available to code running in the system server process itself.
+ *
+ * @return a unique 64-bit token handle which is needed to refer to this token later.
+ */
+ public long addEscrowToken(byte[] token, int userId,
+ @Nullable EscrowTokenStateChangeCallback callback) {
+ return getLockSettingsInternal().addEscrowToken(token, userId, callback);
+ }
+
+ /**
+ * Callback interface to notify when an added escrow token has been activated.
+ */
+ public interface EscrowTokenStateChangeCallback {
+ /**
+ * The method to be called when the token is activated.
+ * @param handle 64 bit handle corresponding to the escrow token
+ * @param userid user for whom the escrow token has been added
+ */
+ void onEscrowTokenActivated(long handle, int userid);
+ }
+
+ /**
+ * Remove an escrow token.
+ *
+ * <p>This method is only available to code running in the system server process itself.
+ *
+ * @return true if the given handle refers to a valid token previously returned from
+ * {@link #addEscrowToken}, whether it's active or not. return false otherwise.
+ */
+ public boolean removeEscrowToken(long handle, int userId) {
+ return getLockSettingsInternal().removeEscrowToken(handle, userId);
+ }
+
+ /**
+ * Check if the given escrow token is active or not. Only active token can be used to call
+ * {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken}
+ *
+ * <p>This method is only available to code running in the system server process itself.
+ */
+ public boolean isEscrowTokenActive(long handle, int userId) {
+ return getLockSettingsInternal().isEscrowTokenActive(handle, userId);
+ }
+
+ /**
+ * Change a user's lock credential with a pre-configured escrow token.
+ *
+ * <p>This method is only available to code running in the system server process itself.
+ *
+ * @param credential The new credential to be set
+ * @param tokenHandle Handle of the escrow token
+ * @param token Escrow token
+ * @param userHandle The user who's lock credential to be changed
+ * @return {@code true} if the operation is successful.
+ */
+ public boolean setLockCredentialWithToken(@NonNull LockscreenCredential credential,
+ long tokenHandle, byte[] token, int userHandle) {
+ if (!hasSecureLockScreen()) {
+ throw new UnsupportedOperationException(
+ "This operation requires the lock screen feature.");
+ }
+ credential.checkLength();
+ LockSettingsInternal localService = getLockSettingsInternal();
+
+ if (!localService.setLockCredentialWithToken(credential, tokenHandle, token, userHandle)) {
+ return false;
+ }
+
+ onPostPasswordChanged(credential, userHandle);
+ return true;
+ }
+
+ /**
+ * Unlock the specified user by an pre-activated escrow token. This should have the same effect
+ * on device encryption as the user entering his lockscreen credentials for the first time after
+ * boot, this includes unlocking the user's credential-encrypted storage as well as the keystore
+ *
+ * <p>This method is only available to code running in the system server process itself.
+ *
+ * @return {@code true} if the supplied token is valid and unlock succeeds,
+ * {@code false} otherwise.
+ */
+ public boolean unlockUserWithToken(long tokenHandle, byte[] token, int userId) {
+ return getLockSettingsInternal().unlockUserWithToken(tokenHandle, token, userId);
+ }
+
+
+ /**
+ * Callback to be notified about progress when checking credentials.
+ */
+ public interface CheckCredentialProgressCallback {
+
+ /**
+ * Called as soon as possible when we know that the credentials match but the user hasn't
+ * been fully unlocked.
+ */
+ void onEarlyMatched();
+ }
+
+ /**
+ * Tracks the global strong authentication state.
+ */
+ public static class StrongAuthTracker {
+
+ @IntDef(flag = true,
+ value = { STRONG_AUTH_NOT_REQUIRED,
+ STRONG_AUTH_REQUIRED_AFTER_BOOT,
+ STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW,
+ SOME_AUTH_REQUIRED_AFTER_USER_REQUEST,
+ STRONG_AUTH_REQUIRED_AFTER_LOCKOUT,
+ STRONG_AUTH_REQUIRED_AFTER_TIMEOUT,
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
+ STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StrongAuthFlags {}
+
+ /**
+ * Strong authentication is not required.
+ */
+ public static final int STRONG_AUTH_NOT_REQUIRED = 0x0;
+
+ /**
+ * Strong authentication is required because the user has not authenticated since boot.
+ */
+ public static final int STRONG_AUTH_REQUIRED_AFTER_BOOT = 0x1;
+
+ /**
+ * Strong authentication is required because a device admin has requested it.
+ */
+ public static final int STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW = 0x2;
+
+ /**
+ * Some authentication is required because the user has temporarily disabled trust.
+ */
+ public static final int SOME_AUTH_REQUIRED_AFTER_USER_REQUEST = 0x4;
+
+ /**
+ * Strong authentication is required because the user has been locked out after too many
+ * attempts.
+ */
+ public static final int STRONG_AUTH_REQUIRED_AFTER_LOCKOUT = 0x8;
+
+ /**
+ * Strong authentication is required because it hasn't been used for a time required by
+ * a device admin.
+ */
+ public static final int STRONG_AUTH_REQUIRED_AFTER_TIMEOUT = 0x10;
+
+ /**
+ * Strong authentication is required because the user has triggered lockdown.
+ */
+ public static final int STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN = 0x20;
+
+ /**
+ * Strong authentication is required to prepare for unattended upgrade.
+ */
+ public static final int STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE = 0x40;
+
+ /**
+ * Strong authentication is required because it hasn't been used for a time after a
+ * non-strong biometric (i.e. weak or convenience biometric) is used to unlock device.
+ */
+ public static final int STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT = 0x80;
+
+ /**
+ * Strong auth flags that do not prevent biometric methods from being accepted as auth.
+ * If any other flags are set, biometric authentication is disabled.
+ */
+ private static final int ALLOWING_BIOMETRIC = STRONG_AUTH_NOT_REQUIRED
+ | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
+
+ private final SparseIntArray mStrongAuthRequiredForUser = new SparseIntArray();
+ private final H mHandler;
+ private final int mDefaultStrongAuthFlags;
+
+ private final SparseBooleanArray mIsNonStrongBiometricAllowedForUser =
+ new SparseBooleanArray();
+ private final boolean mDefaultIsNonStrongBiometricAllowed = true;
+
+ public StrongAuthTracker(Context context) {
+ this(context, Looper.myLooper());
+ }
+
+ /**
+ * @param looper the looper on whose thread calls to {@link #onStrongAuthRequiredChanged}
+ * will be scheduled.
+ * @param context the current {@link Context}
+ */
+ public StrongAuthTracker(Context context, Looper looper) {
+ mHandler = new H(looper);
+ mDefaultStrongAuthFlags = getDefaultFlags(context);
+ }
+
+ public static @StrongAuthFlags int getDefaultFlags(Context context) {
+ boolean strongAuthRequired = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_strongAuthRequiredOnBoot);
+ return strongAuthRequired ? STRONG_AUTH_REQUIRED_AFTER_BOOT : STRONG_AUTH_NOT_REQUIRED;
+ }
+
+ /**
+ * Returns {@link #STRONG_AUTH_NOT_REQUIRED} if strong authentication is not required,
+ * otherwise returns a combination of {@link StrongAuthFlags} indicating why strong
+ * authentication is required.
+ *
+ * @param userId the user for whom the state is queried.
+ */
+ public @StrongAuthFlags int getStrongAuthForUser(int userId) {
+ return mStrongAuthRequiredForUser.get(userId, mDefaultStrongAuthFlags);
+ }
+
+ /**
+ * @return true if unlocking with trust alone is allowed for {@code userId} by the current
+ * strong authentication requirements.
+ */
+ public boolean isTrustAllowedForUser(int userId) {
+ return getStrongAuthForUser(userId) == STRONG_AUTH_NOT_REQUIRED;
+ }
+
+ /**
+ * @return true if unlocking with a biometric method alone is allowed for {@code userId}
+ * by the current strong authentication requirements.
+ */
+ public boolean isBiometricAllowedForUser(boolean isStrongBiometric, int userId) {
+ boolean allowed = ((getStrongAuthForUser(userId) & ~ALLOWING_BIOMETRIC) == 0);
+ if (!isStrongBiometric) {
+ allowed &= isNonStrongBiometricAllowedAfterIdleTimeout(userId);
+ }
+ return allowed;
+ }
+
+ /**
+ * @return true if unlocking with a non-strong (i.e. weak or convenience) biometric method
+ * alone is allowed for {@code userId}, otherwise returns false.
+ */
+ public boolean isNonStrongBiometricAllowedAfterIdleTimeout(int userId) {
+ return mIsNonStrongBiometricAllowedForUser.get(userId,
+ mDefaultIsNonStrongBiometricAllowed);
+ }
+
+ /**
+ * Called when the strong authentication requirements for {@code userId} changed.
+ */
+ public void onStrongAuthRequiredChanged(int userId) {
+ }
+
+ /**
+ * Called when whether non-strong biometric is allowed for {@code userId} changed.
+ */
+ public void onIsNonStrongBiometricAllowedChanged(int userId) {
+ }
+
+ protected void handleStrongAuthRequiredChanged(@StrongAuthFlags int strongAuthFlags,
+ int userId) {
+ int oldValue = getStrongAuthForUser(userId);
+ if (strongAuthFlags != oldValue) {
+ if (strongAuthFlags == mDefaultStrongAuthFlags) {
+ mStrongAuthRequiredForUser.delete(userId);
+ } else {
+ mStrongAuthRequiredForUser.put(userId, strongAuthFlags);
+ }
+ onStrongAuthRequiredChanged(userId);
+ }
+ }
+
+ protected void handleIsNonStrongBiometricAllowedChanged(boolean allowed,
+ int userId) {
+ boolean oldValue = isNonStrongBiometricAllowedAfterIdleTimeout(userId);
+ if (allowed != oldValue) {
+ if (allowed == mDefaultIsNonStrongBiometricAllowed) {
+ mIsNonStrongBiometricAllowedForUser.delete(userId);
+ } else {
+ mIsNonStrongBiometricAllowedForUser.put(userId, allowed);
+ }
+ onIsNonStrongBiometricAllowedChanged(userId);
+ }
+ }
+
+ protected final IStrongAuthTracker.Stub mStub = new IStrongAuthTracker.Stub() {
+ @Override
+ public void onStrongAuthRequiredChanged(@StrongAuthFlags int strongAuthFlags,
+ int userId) {
+ mHandler.obtainMessage(H.MSG_ON_STRONG_AUTH_REQUIRED_CHANGED,
+ strongAuthFlags, userId).sendToTarget();
+ }
+
+ @Override
+ public void onIsNonStrongBiometricAllowedChanged(boolean allowed, int userId) {
+ mHandler.obtainMessage(H.MSG_ON_IS_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED,
+ allowed ? 1 : 0, userId).sendToTarget();
+ }
+ };
+
+ private class H extends Handler {
+ static final int MSG_ON_STRONG_AUTH_REQUIRED_CHANGED = 1;
+ static final int MSG_ON_IS_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED = 2;
+
+ public H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ON_STRONG_AUTH_REQUIRED_CHANGED:
+ handleStrongAuthRequiredChanged(msg.arg1, msg.arg2);
+ break;
+ case MSG_ON_IS_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED:
+ handleIsNonStrongBiometricAllowedChanged(msg.arg1 == 1 /* allowed */,
+ msg.arg2);
+ break;
+ }
+ }
+ }
+ }
+
+ public void enableSyntheticPassword() {
+ setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1L, UserHandle.USER_SYSTEM);
+ }
+
+ public void disableSyntheticPassword() {
+ setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0L, UserHandle.USER_SYSTEM);
+ }
+
+ public boolean isSyntheticPasswordEnabled() {
+ return getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, SYNTHETIC_PASSWORD_ENABLED_BY_DEFAULT,
+ UserHandle.USER_SYSTEM) != 0;
+ }
+
+ /**
+ * Returns whether the given user has pending escrow tokens
+ */
+ public boolean hasPendingEscrowToken(int userId) {
+ try {
+ return getLockSettings().hasPendingEscrowToken(userId);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if the device supports the lock screen feature, false otherwise.
+ */
+ public boolean hasSecureLockScreen() {
+ if (mHasSecureLockScreen == null) {
+ try {
+ mHasSecureLockScreen = Boolean.valueOf(getLockSettings().hasSecureLockScreen());
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return mHasSecureLockScreen.booleanValue();
+ }
+
+ public static boolean userOwnsFrpCredential(Context context, UserInfo info) {
+ return info != null && info.isPrimary() && info.isAdmin() && frpCredentialEnabled(context);
+ }
+
+ public static boolean frpCredentialEnabled(Context context) {
+ return FRP_CREDENTIAL_ENABLED && context.getResources().getBoolean(
+ com.android.internal.R.bool.config_enableCredentialFactoryResetProtection);
+ }
+
+ /**
+ * Attempt to rederive the unified work challenge for the specified profile user and unlock the
+ * user. If successful, this would allow the user to leave quiet mode automatically without
+ * additional user authentication.
+ *
+ * This is made possible by the framework storing an encrypted copy of the unified challenge
+ * auth-bound to the primary user's lockscreen. As long as the primery user has unlocked
+ * recently (7 days), the framework will be able to decrypt it and plug the secret into the
+ * unlock flow.
+ *
+ * @return {@code true} if automatic unlocking is successful, {@code false} otherwise.
+ */
+ public boolean tryUnlockWithCachedUnifiedChallenge(int userId) {
+ try {
+ return getLockSettings().tryUnlockWithCachedUnifiedChallenge(userId);
+ } catch (RemoteException re) {
+ return false;
+ }
+ }
+
+ /** Remove cached unified profile challenge, for testing and CTS usage. */
+ public void removeCachedUnifiedChallenge(int userId) {
+ try {
+ getLockSettings().removeCachedUnifiedChallenge(userId);
+ } catch (RemoteException re) {
+ re.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/com/android/internal/widget/LockPatternView.java b/com/android/internal/widget/LockPatternView.java
new file mode 100644
index 0000000..4ddc782
--- /dev/null
+++ b/com/android/internal/widget/LockPatternView.java
@@ -0,0 +1,1589 @@
+/*
+ * Copyright (C) 2007 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.RenderNodeAnimator;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays and detects the user's unlock attempt, which is a drag of a finger
+ * across 9 regions of the screen.
+ *
+ * Is also capable of displaying a static pattern in "in progress", "wrong" or
+ * "correct" states.
+ */
+public class LockPatternView extends View {
+ // Aspect to use when rendering this view
+ private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height
+ private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h)
+ private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h)
+
+ private static final boolean PROFILE_DRAWING = false;
+ private static final float LINE_FADE_ALPHA_MULTIPLIER = 1.5f;
+ private final CellState[][] mCellStates;
+
+ private final int mDotSize;
+ private final int mDotSizeActivated;
+ private final int mPathWidth;
+
+ private boolean mDrawingProfilingStarted = false;
+
+ @UnsupportedAppUsage
+ private final Paint mPaint = new Paint();
+ @UnsupportedAppUsage
+ private final Paint mPathPaint = new Paint();
+
+ /**
+ * How many milliseconds we spend animating each circle of a lock pattern
+ * if the animating mode is set. The entire animation should take this
+ * constant * the length of the pattern to complete.
+ */
+ private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;
+
+ /**
+ * This can be used to avoid updating the display for very small motions or noisy panels.
+ * It didn't seem to have much impact on the devices tested, so currently set to 0.
+ */
+ private static final float DRAG_THRESHHOLD = 0.0f;
+ public static final int VIRTUAL_BASE_VIEW_ID = 1;
+ public static final boolean DEBUG_A11Y = false;
+ private static final String TAG = "LockPatternView";
+
+ private OnPatternListener mOnPatternListener;
+ @UnsupportedAppUsage
+ private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
+
+ /**
+ * Lookup table for the circles of the pattern we are currently drawing.
+ * This will be the cells of the complete pattern unless we are animating,
+ * in which case we use this to hold the cells we are drawing for the in
+ * progress animation.
+ */
+ private final boolean[][] mPatternDrawLookup = new boolean[3][3];
+
+ /**
+ * the in progress point:
+ * - during interaction: where the user's finger is
+ * - during animation: the current tip of the animating line
+ */
+ private float mInProgressX = -1;
+ private float mInProgressY = -1;
+
+ private long mAnimatingPeriodStart;
+ private long[] mLineFadeStart = new long[9];
+
+ @UnsupportedAppUsage
+ private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
+ private boolean mInputEnabled = true;
+ @UnsupportedAppUsage
+ private boolean mInStealthMode = false;
+ private boolean mEnableHapticFeedback = true;
+ @UnsupportedAppUsage
+ private boolean mPatternInProgress = false;
+ private boolean mFadePattern = true;
+
+ private float mHitFactor = 0.6f;
+
+ @UnsupportedAppUsage
+ private float mSquareWidth;
+ @UnsupportedAppUsage
+ private float mSquareHeight;
+
+ private final Path mCurrentPath = new Path();
+ private final Rect mInvalidate = new Rect();
+ private final Rect mTmpInvalidateRect = new Rect();
+
+ private int mAspect;
+ private int mRegularColor;
+ private int mErrorColor;
+ private int mSuccessColor;
+
+ private final Interpolator mFastOutSlowInInterpolator;
+ private final Interpolator mLinearOutSlowInInterpolator;
+ private PatternExploreByTouchHelper mExploreByTouchHelper;
+ private AudioManager mAudioManager;
+
+ private Drawable mSelectedDrawable;
+ private Drawable mNotSelectedDrawable;
+ private boolean mUseLockPatternDrawable;
+
+ /**
+ * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
+ */
+ public static final class Cell {
+ @UnsupportedAppUsage
+ final int row;
+ @UnsupportedAppUsage
+ final int column;
+
+ // keep # objects limited to 9
+ private static final Cell[][] sCells = createCells();
+
+ private static Cell[][] createCells() {
+ Cell[][] res = new Cell[3][3];
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ res[i][j] = new Cell(i, j);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * @param row The row of the cell.
+ * @param column The column of the cell.
+ */
+ private Cell(int row, int column) {
+ checkRange(row, column);
+ this.row = row;
+ this.column = column;
+ }
+
+ public int getRow() {
+ return row;
+ }
+
+ public int getColumn() {
+ return column;
+ }
+
+ public static Cell of(int row, int column) {
+ checkRange(row, column);
+ return sCells[row][column];
+ }
+
+ private static void checkRange(int row, int column) {
+ if (row < 0 || row > 2) {
+ throw new IllegalArgumentException("row must be in range 0-2");
+ }
+ if (column < 0 || column > 2) {
+ throw new IllegalArgumentException("column must be in range 0-2");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "(row=" + row + ",clmn=" + column + ")";
+ }
+ }
+
+ public static class CellState {
+ int row;
+ int col;
+ boolean hwAnimating;
+ CanvasProperty<Float> hwRadius;
+ CanvasProperty<Float> hwCenterX;
+ CanvasProperty<Float> hwCenterY;
+ CanvasProperty<Paint> hwPaint;
+ float radius;
+ float translationY;
+ float alpha = 1f;
+ public float lineEndX = Float.MIN_VALUE;
+ public float lineEndY = Float.MIN_VALUE;
+ public ValueAnimator lineAnimator;
+ }
+
+ /**
+ * How to display the current pattern.
+ */
+ public enum DisplayMode {
+
+ /**
+ * The pattern drawn is correct (i.e draw it in a friendly color)
+ */
+ @UnsupportedAppUsage
+ Correct,
+
+ /**
+ * Animate the pattern (for demo, and help).
+ */
+ @UnsupportedAppUsage
+ Animate,
+
+ /**
+ * The pattern is wrong (i.e draw a foreboding color)
+ */
+ @UnsupportedAppUsage
+ Wrong
+ }
+
+ /**
+ * The call back interface for detecting patterns entered by the user.
+ */
+ public static interface OnPatternListener {
+
+ /**
+ * A new pattern has begun.
+ */
+ void onPatternStart();
+
+ /**
+ * The pattern was cleared.
+ */
+ void onPatternCleared();
+
+ /**
+ * The user extended the pattern currently being drawn by one cell.
+ * @param pattern The pattern with newly added cell.
+ */
+ void onPatternCellAdded(List<Cell> pattern);
+
+ /**
+ * A pattern was detected from the user.
+ * @param pattern The pattern.
+ */
+ void onPatternDetected(List<Cell> pattern);
+ }
+
+ public LockPatternView(Context context) {
+ this(context, null);
+ }
+
+ @UnsupportedAppUsage
+ public LockPatternView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView,
+ R.attr.lockPatternStyle, R.style.Widget_LockPatternView);
+
+ final String aspect = a.getString(R.styleable.LockPatternView_aspect);
+
+ if ("square".equals(aspect)) {
+ mAspect = ASPECT_SQUARE;
+ } else if ("lock_width".equals(aspect)) {
+ mAspect = ASPECT_LOCK_WIDTH;
+ } else if ("lock_height".equals(aspect)) {
+ mAspect = ASPECT_LOCK_HEIGHT;
+ } else {
+ mAspect = ASPECT_SQUARE;
+ }
+
+ setClickable(true);
+
+
+ mPathPaint.setAntiAlias(true);
+ mPathPaint.setDither(true);
+
+ mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, 0);
+ mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, 0);
+ mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, 0);
+
+ int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor);
+ mPathPaint.setColor(pathColor);
+
+ mPathPaint.setStyle(Paint.Style.STROKE);
+ mPathPaint.setStrokeJoin(Paint.Join.ROUND);
+ mPathPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);
+ mPathPaint.setStrokeWidth(mPathWidth);
+
+ mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
+ mDotSizeActivated = getResources().getDimensionPixelSize(
+ R.dimen.lock_pattern_dot_size_activated);
+
+ mUseLockPatternDrawable = getResources().getBoolean(R.bool.use_lock_pattern_drawable);
+ if (mUseLockPatternDrawable) {
+ mSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_selected);
+ mNotSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_notselected);
+ }
+
+ mPaint.setAntiAlias(true);
+ mPaint.setDither(true);
+
+ mCellStates = new CellState[3][3];
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ mCellStates[i][j] = new CellState();
+ mCellStates[i][j].radius = mDotSize/2;
+ mCellStates[i][j].row = i;
+ mCellStates[i][j].col = j;
+ }
+ }
+
+ mFastOutSlowInInterpolator =
+ AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
+ mLinearOutSlowInInterpolator =
+ AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
+ mExploreByTouchHelper = new PatternExploreByTouchHelper(this);
+ setAccessibilityDelegate(mExploreByTouchHelper);
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ a.recycle();
+ }
+
+ @UnsupportedAppUsage
+ public CellState[][] getCellStates() {
+ return mCellStates;
+ }
+
+ /**
+ * @return Whether the view is in stealth mode.
+ */
+ public boolean isInStealthMode() {
+ return mInStealthMode;
+ }
+
+ /**
+ * @return Whether the view has tactile feedback enabled.
+ */
+ public boolean isTactileFeedbackEnabled() {
+ return mEnableHapticFeedback;
+ }
+
+ /**
+ * Set whether the view is in stealth mode. If true, there will be no
+ * visible feedback as the user enters the pattern.
+ *
+ * @param inStealthMode Whether in stealth mode.
+ */
+ @UnsupportedAppUsage
+ public void setInStealthMode(boolean inStealthMode) {
+ mInStealthMode = inStealthMode;
+ }
+
+ /**
+ * Set whether the pattern should fade as it's being drawn. If
+ * true, each segment of the pattern fades over time.
+ */
+ public void setFadePattern(boolean fadePattern) {
+ mFadePattern = fadePattern;
+ }
+
+ /**
+ * Set whether the view will use tactile feedback. If true, there will be
+ * tactile feedback as the user enters the pattern.
+ *
+ * @param tactileFeedbackEnabled Whether tactile feedback is enabled
+ */
+ @UnsupportedAppUsage
+ public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
+ mEnableHapticFeedback = tactileFeedbackEnabled;
+ }
+
+ /**
+ * Set the call back for pattern detection.
+ * @param onPatternListener The call back.
+ */
+ @UnsupportedAppUsage
+ public void setOnPatternListener(
+ OnPatternListener onPatternListener) {
+ mOnPatternListener = onPatternListener;
+ }
+
+ /**
+ * Set the pattern explicitely (rather than waiting for the user to input
+ * a pattern).
+ * @param displayMode How to display the pattern.
+ * @param pattern The pattern.
+ */
+ public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
+ mPattern.clear();
+ mPattern.addAll(pattern);
+ clearPatternDrawLookup();
+ for (Cell cell : pattern) {
+ mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
+ }
+
+ setDisplayMode(displayMode);
+ }
+
+ /**
+ * Set the display mode of the current pattern. This can be useful, for
+ * instance, after detecting a pattern to tell this view whether change the
+ * in progress result to correct or wrong.
+ * @param displayMode The display mode.
+ */
+ @UnsupportedAppUsage
+ public void setDisplayMode(DisplayMode displayMode) {
+ mPatternDisplayMode = displayMode;
+ if (displayMode == DisplayMode.Animate) {
+ if (mPattern.size() == 0) {
+ throw new IllegalStateException("you must have a pattern to "
+ + "animate if you want to set the display mode to animate");
+ }
+ mAnimatingPeriodStart = SystemClock.elapsedRealtime();
+ final Cell first = mPattern.get(0);
+ mInProgressX = getCenterXForColumn(first.getColumn());
+ mInProgressY = getCenterYForRow(first.getRow());
+ clearPatternDrawLookup();
+ }
+ invalidate();
+ }
+
+ public void startCellStateAnimation(CellState cellState, float startAlpha, float endAlpha,
+ float startTranslationY, float endTranslationY, float startScale, float endScale,
+ long delay, long duration,
+ Interpolator interpolator, Runnable finishRunnable) {
+ if (isHardwareAccelerated()) {
+ startCellStateAnimationHw(cellState, startAlpha, endAlpha, startTranslationY,
+ endTranslationY, startScale, endScale, delay, duration, interpolator,
+ finishRunnable);
+ } else {
+ startCellStateAnimationSw(cellState, startAlpha, endAlpha, startTranslationY,
+ endTranslationY, startScale, endScale, delay, duration, interpolator,
+ finishRunnable);
+ }
+ }
+
+ private void startCellStateAnimationSw(final CellState cellState,
+ final float startAlpha, final float endAlpha,
+ final float startTranslationY, final float endTranslationY,
+ final float startScale, final float endScale,
+ long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) {
+ cellState.alpha = startAlpha;
+ cellState.translationY = startTranslationY;
+ cellState.radius = mDotSize/2 * startScale;
+ ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+ animator.setDuration(duration);
+ animator.setStartDelay(delay);
+ animator.setInterpolator(interpolator);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float t = (float) animation.getAnimatedValue();
+ cellState.alpha = (1 - t) * startAlpha + t * endAlpha;
+ cellState.translationY = (1 - t) * startTranslationY + t * endTranslationY;
+ cellState.radius = mDotSize/2 * ((1 - t) * startScale + t * endScale);
+ invalidate();
+ }
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (finishRunnable != null) {
+ finishRunnable.run();
+ }
+ }
+ });
+ animator.start();
+ }
+
+ private void startCellStateAnimationHw(final CellState cellState,
+ float startAlpha, float endAlpha,
+ float startTranslationY, float endTranslationY,
+ float startScale, float endScale,
+ long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) {
+ cellState.alpha = endAlpha;
+ cellState.translationY = endTranslationY;
+ cellState.radius = mDotSize/2 * endScale;
+ cellState.hwAnimating = true;
+ cellState.hwCenterY = CanvasProperty.createFloat(
+ getCenterYForRow(cellState.row) + startTranslationY);
+ cellState.hwCenterX = CanvasProperty.createFloat(getCenterXForColumn(cellState.col));
+ cellState.hwRadius = CanvasProperty.createFloat(mDotSize/2 * startScale);
+ mPaint.setColor(getCurrentColor(false));
+ mPaint.setAlpha((int) (startAlpha * 255));
+ cellState.hwPaint = CanvasProperty.createPaint(new Paint(mPaint));
+
+ startRtFloatAnimation(cellState.hwCenterY,
+ getCenterYForRow(cellState.row) + endTranslationY, delay, duration, interpolator);
+ startRtFloatAnimation(cellState.hwRadius, mDotSize/2 * endScale, delay, duration,
+ interpolator);
+ startRtAlphaAnimation(cellState, endAlpha, delay, duration, interpolator,
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ cellState.hwAnimating = false;
+ if (finishRunnable != null) {
+ finishRunnable.run();
+ }
+ }
+ });
+
+ invalidate();
+ }
+
+ private void startRtAlphaAnimation(CellState cellState, float endAlpha,
+ long delay, long duration, Interpolator interpolator,
+ Animator.AnimatorListener listener) {
+ RenderNodeAnimator animator = new RenderNodeAnimator(cellState.hwPaint,
+ RenderNodeAnimator.PAINT_ALPHA, (int) (endAlpha * 255));
+ animator.setDuration(duration);
+ animator.setStartDelay(delay);
+ animator.setInterpolator(interpolator);
+ animator.setTarget(this);
+ animator.addListener(listener);
+ animator.start();
+ }
+
+ private void startRtFloatAnimation(CanvasProperty<Float> property, float endValue,
+ long delay, long duration, Interpolator interpolator) {
+ RenderNodeAnimator animator = new RenderNodeAnimator(property, endValue);
+ animator.setDuration(duration);
+ animator.setStartDelay(delay);
+ animator.setInterpolator(interpolator);
+ animator.setTarget(this);
+ animator.start();
+ }
+
+ private void notifyCellAdded() {
+ // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
+ if (mOnPatternListener != null) {
+ mOnPatternListener.onPatternCellAdded(mPattern);
+ }
+ // Disable used cells for accessibility as they get added
+ if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added.");
+ mExploreByTouchHelper.invalidateRoot();
+ }
+
+ private void notifyPatternStarted() {
+ sendAccessEvent(R.string.lockscreen_access_pattern_start);
+ if (mOnPatternListener != null) {
+ mOnPatternListener.onPatternStart();
+ }
+ }
+
+ @UnsupportedAppUsage
+ private void notifyPatternDetected() {
+ sendAccessEvent(R.string.lockscreen_access_pattern_detected);
+ if (mOnPatternListener != null) {
+ mOnPatternListener.onPatternDetected(mPattern);
+ }
+ }
+
+ private void notifyPatternCleared() {
+ sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
+ if (mOnPatternListener != null) {
+ mOnPatternListener.onPatternCleared();
+ }
+ }
+
+ /**
+ * Clear the pattern.
+ */
+ @UnsupportedAppUsage
+ public void clearPattern() {
+ resetPattern();
+ }
+
+ @Override
+ protected boolean dispatchHoverEvent(MotionEvent event) {
+ // Dispatch to onHoverEvent first so mPatternInProgress is up to date when the
+ // helper gets the event.
+ boolean handled = super.dispatchHoverEvent(event);
+ handled |= mExploreByTouchHelper.dispatchHoverEvent(event);
+ return handled;
+ }
+
+ /**
+ * Reset all pattern state.
+ */
+ private void resetPattern() {
+ mPattern.clear();
+ clearPatternDrawLookup();
+ mPatternDisplayMode = DisplayMode.Correct;
+ invalidate();
+ }
+
+ /**
+ * If there are any cells being drawn.
+ */
+ public boolean isEmpty() {
+ return mPattern.isEmpty();
+ }
+
+ /**
+ * Clear the pattern lookup table. Also reset the line fade start times for
+ * the next attempt.
+ */
+ private void clearPatternDrawLookup() {
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ mPatternDrawLookup[i][j] = false;
+ mLineFadeStart[i+j*3] = 0;
+ }
+ }
+ }
+
+ /**
+ * Disable input (for instance when displaying a message that will
+ * timeout so user doesn't get view into messy state).
+ */
+ @UnsupportedAppUsage
+ public void disableInput() {
+ mInputEnabled = false;
+ }
+
+ /**
+ * Enable input.
+ */
+ @UnsupportedAppUsage
+ public void enableInput() {
+ mInputEnabled = true;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ final int width = w - mPaddingLeft - mPaddingRight;
+ mSquareWidth = width / 3.0f;
+
+ if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")");
+ final int height = h - mPaddingTop - mPaddingBottom;
+ mSquareHeight = height / 3.0f;
+ mExploreByTouchHelper.invalidateRoot();
+
+ if (mUseLockPatternDrawable) {
+ mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
+ mSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
+ }
+ }
+
+ private int resolveMeasured(int measureSpec, int desired)
+ {
+ int result = 0;
+ int specSize = MeasureSpec.getSize(measureSpec);
+ switch (MeasureSpec.getMode(measureSpec)) {
+ case MeasureSpec.UNSPECIFIED:
+ result = desired;
+ break;
+ case MeasureSpec.AT_MOST:
+ result = Math.max(specSize, desired);
+ break;
+ case MeasureSpec.EXACTLY:
+ default:
+ result = specSize;
+ }
+ return result;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int minimumWidth = getSuggestedMinimumWidth();
+ final int minimumHeight = getSuggestedMinimumHeight();
+ int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
+ int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
+
+ switch (mAspect) {
+ case ASPECT_SQUARE:
+ viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
+ break;
+ case ASPECT_LOCK_WIDTH:
+ viewHeight = Math.min(viewWidth, viewHeight);
+ break;
+ case ASPECT_LOCK_HEIGHT:
+ viewWidth = Math.min(viewWidth, viewHeight);
+ break;
+ }
+ // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
+ setMeasuredDimension(viewWidth, viewHeight);
+ }
+
+ /**
+ * Determines whether the point x, y will add a new point to the current
+ * pattern (in addition to finding the cell, also makes heuristic choices
+ * such as filling in gaps based on current pattern).
+ * @param x The x coordinate.
+ * @param y The y coordinate.
+ */
+ private Cell detectAndAddHit(float x, float y) {
+ final Cell cell = checkForNewHit(x, y);
+ if (cell != null) {
+
+ // check for gaps in existing pattern
+ Cell fillInGapCell = null;
+ final ArrayList<Cell> pattern = mPattern;
+ if (!pattern.isEmpty()) {
+ final Cell lastCell = pattern.get(pattern.size() - 1);
+ int dRow = cell.row - lastCell.row;
+ int dColumn = cell.column - lastCell.column;
+
+ int fillInRow = lastCell.row;
+ int fillInColumn = lastCell.column;
+
+ if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
+ fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
+ }
+
+ if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
+ fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
+ }
+
+ fillInGapCell = Cell.of(fillInRow, fillInColumn);
+ }
+
+ if (fillInGapCell != null &&
+ !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
+ addCellToPattern(fillInGapCell);
+ }
+ addCellToPattern(cell);
+ if (mEnableHapticFeedback) {
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+ | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
+ }
+ return cell;
+ }
+ return null;
+ }
+
+ private void addCellToPattern(Cell newCell) {
+ mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
+ mPattern.add(newCell);
+ if (!mInStealthMode) {
+ startCellActivatedAnimation(newCell);
+ }
+ notifyCellAdded();
+ }
+
+ private void startCellActivatedAnimation(Cell cell) {
+ final CellState cellState = mCellStates[cell.row][cell.column];
+ startRadiusAnimation(mDotSize/2, mDotSizeActivated/2, 96, mLinearOutSlowInInterpolator,
+ cellState, new Runnable() {
+ @Override
+ public void run() {
+ startRadiusAnimation(mDotSizeActivated/2, mDotSize/2, 192,
+ mFastOutSlowInInterpolator,
+ cellState, null);
+ }
+ });
+ startLineEndAnimation(cellState, mInProgressX, mInProgressY,
+ getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
+ }
+
+ private void startLineEndAnimation(final CellState state,
+ final float startX, final float startY, final float targetX, final float targetY) {
+ ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
+ valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float t = (float) animation.getAnimatedValue();
+ state.lineEndX = (1 - t) * startX + t * targetX;
+ state.lineEndY = (1 - t) * startY + t * targetY;
+ invalidate();
+ }
+ });
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ state.lineAnimator = null;
+ }
+ });
+ valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
+ valueAnimator.setDuration(100);
+ valueAnimator.start();
+ state.lineAnimator = valueAnimator;
+ }
+
+ private void startRadiusAnimation(float start, float end, long duration,
+ Interpolator interpolator, final CellState state, final Runnable endRunnable) {
+ ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
+ valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ state.radius = (float) animation.getAnimatedValue();
+ invalidate();
+ }
+ });
+ if (endRunnable != null) {
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ endRunnable.run();
+ }
+ });
+ }
+ valueAnimator.setInterpolator(interpolator);
+ valueAnimator.setDuration(duration);
+ valueAnimator.start();
+ }
+
+ // helper method to find which cell a point maps to
+ private Cell checkForNewHit(float x, float y) {
+
+ final int rowHit = getRowHit(y);
+ if (rowHit < 0) {
+ return null;
+ }
+ final int columnHit = getColumnHit(x);
+ if (columnHit < 0) {
+ return null;
+ }
+
+ if (mPatternDrawLookup[rowHit][columnHit]) {
+ return null;
+ }
+ return Cell.of(rowHit, columnHit);
+ }
+
+ /**
+ * Helper method to find the row that y falls into.
+ * @param y The y coordinate
+ * @return The row that y falls in, or -1 if it falls in no row.
+ */
+ private int getRowHit(float y) {
+
+ final float squareHeight = mSquareHeight;
+ float hitSize = squareHeight * mHitFactor;
+
+ float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
+ for (int i = 0; i < 3; i++) {
+
+ final float hitTop = offset + squareHeight * i;
+ if (y >= hitTop && y <= hitTop + hitSize) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Helper method to find the column x fallis into.
+ * @param x The x coordinate.
+ * @return The column that x falls in, or -1 if it falls in no column.
+ */
+ private int getColumnHit(float x) {
+ final float squareWidth = mSquareWidth;
+ float hitSize = squareWidth * mHitFactor;
+
+ float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
+ for (int i = 0; i < 3; i++) {
+
+ final float hitLeft = offset + squareWidth * i;
+ if (x >= hitLeft && x <= hitLeft + hitSize) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ event.setAction(MotionEvent.ACTION_DOWN);
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ event.setAction(MotionEvent.ACTION_MOVE);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ event.setAction(MotionEvent.ACTION_UP);
+ break;
+ }
+ onTouchEvent(event);
+ event.setAction(action);
+ }
+ return super.onHoverEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!mInputEnabled || !isEnabled()) {
+ return false;
+ }
+
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ handleActionDown(event);
+ return true;
+ case MotionEvent.ACTION_UP:
+ handleActionUp();
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ handleActionMove(event);
+ return true;
+ case MotionEvent.ACTION_CANCEL:
+ if (mPatternInProgress) {
+ setPatternInProgress(false);
+ resetPattern();
+ notifyPatternCleared();
+ }
+ if (PROFILE_DRAWING) {
+ if (mDrawingProfilingStarted) {
+ Debug.stopMethodTracing();
+ mDrawingProfilingStarted = false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void setPatternInProgress(boolean progress) {
+ mPatternInProgress = progress;
+ mExploreByTouchHelper.invalidateRoot();
+ }
+
+ private void handleActionMove(MotionEvent event) {
+ // Handle all recent motion events so we don't skip any cells even when the device
+ // is busy...
+ final float radius = mPathWidth;
+ final int historySize = event.getHistorySize();
+ mTmpInvalidateRect.setEmpty();
+ boolean invalidateNow = false;
+ for (int i = 0; i < historySize + 1; i++) {
+ final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
+ final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
+ Cell hitCell = detectAndAddHit(x, y);
+ final int patternSize = mPattern.size();
+ if (hitCell != null && patternSize == 1) {
+ setPatternInProgress(true);
+ notifyPatternStarted();
+ }
+ // note current x and y for rubber banding of in progress patterns
+ final float dx = Math.abs(x - mInProgressX);
+ final float dy = Math.abs(y - mInProgressY);
+ if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
+ invalidateNow = true;
+ }
+
+ if (mPatternInProgress && patternSize > 0) {
+ final ArrayList<Cell> pattern = mPattern;
+ final Cell lastCell = pattern.get(patternSize - 1);
+ float lastCellCenterX = getCenterXForColumn(lastCell.column);
+ float lastCellCenterY = getCenterYForRow(lastCell.row);
+
+ // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
+ float left = Math.min(lastCellCenterX, x) - radius;
+ float right = Math.max(lastCellCenterX, x) + radius;
+ float top = Math.min(lastCellCenterY, y) - radius;
+ float bottom = Math.max(lastCellCenterY, y) + radius;
+
+ // Invalidate between the pattern's new cell and the pattern's previous cell
+ if (hitCell != null) {
+ final float width = mSquareWidth * 0.5f;
+ final float height = mSquareHeight * 0.5f;
+ final float hitCellCenterX = getCenterXForColumn(hitCell.column);
+ final float hitCellCenterY = getCenterYForRow(hitCell.row);
+
+ left = Math.min(hitCellCenterX - width, left);
+ right = Math.max(hitCellCenterX + width, right);
+ top = Math.min(hitCellCenterY - height, top);
+ bottom = Math.max(hitCellCenterY + height, bottom);
+ }
+
+ // Invalidate between the pattern's last cell and the previous location
+ mTmpInvalidateRect.union(Math.round(left), Math.round(top),
+ Math.round(right), Math.round(bottom));
+ }
+ }
+ mInProgressX = event.getX();
+ mInProgressY = event.getY();
+
+ // To save updates, we only invalidate if the user moved beyond a certain amount.
+ if (invalidateNow) {
+ mInvalidate.union(mTmpInvalidateRect);
+ invalidate(mInvalidate);
+ mInvalidate.set(mTmpInvalidateRect);
+ }
+ }
+
+ private void sendAccessEvent(int resId) {
+ announceForAccessibility(mContext.getString(resId));
+ }
+
+ private void handleActionUp() {
+ // report pattern detected
+ if (!mPattern.isEmpty()) {
+ setPatternInProgress(false);
+ cancelLineAnimations();
+ notifyPatternDetected();
+ // Also clear pattern if fading is enabled
+ if (mFadePattern) {
+ clearPatternDrawLookup();
+ mPatternDisplayMode = DisplayMode.Correct;
+ }
+ invalidate();
+ }
+ if (PROFILE_DRAWING) {
+ if (mDrawingProfilingStarted) {
+ Debug.stopMethodTracing();
+ mDrawingProfilingStarted = false;
+ }
+ }
+ }
+
+ private void cancelLineAnimations() {
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ CellState state = mCellStates[i][j];
+ if (state.lineAnimator != null) {
+ state.lineAnimator.cancel();
+ state.lineEndX = Float.MIN_VALUE;
+ state.lineEndY = Float.MIN_VALUE;
+ }
+ }
+ }
+ }
+ private void handleActionDown(MotionEvent event) {
+ resetPattern();
+ final float x = event.getX();
+ final float y = event.getY();
+ final Cell hitCell = detectAndAddHit(x, y);
+ if (hitCell != null) {
+ setPatternInProgress(true);
+ mPatternDisplayMode = DisplayMode.Correct;
+ notifyPatternStarted();
+ } else if (mPatternInProgress) {
+ setPatternInProgress(false);
+ notifyPatternCleared();
+ }
+ if (hitCell != null) {
+ final float startX = getCenterXForColumn(hitCell.column);
+ final float startY = getCenterYForRow(hitCell.row);
+
+ final float widthOffset = mSquareWidth / 2f;
+ final float heightOffset = mSquareHeight / 2f;
+
+ invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
+ (int) (startX + widthOffset), (int) (startY + heightOffset));
+ }
+ mInProgressX = x;
+ mInProgressY = y;
+ if (PROFILE_DRAWING) {
+ if (!mDrawingProfilingStarted) {
+ Debug.startMethodTracing("LockPatternDrawing");
+ mDrawingProfilingStarted = true;
+ }
+ }
+ }
+
+ private float getCenterXForColumn(int column) {
+ return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
+ }
+
+ private float getCenterYForRow(int row) {
+ return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final ArrayList<Cell> pattern = mPattern;
+ final int count = pattern.size();
+ final boolean[][] drawLookup = mPatternDrawLookup;
+
+ if (mPatternDisplayMode == DisplayMode.Animate) {
+
+ // figure out which circles to draw
+
+ // + 1 so we pause on complete pattern
+ final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
+ final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
+ mAnimatingPeriodStart) % oneCycle;
+ final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
+
+ clearPatternDrawLookup();
+ for (int i = 0; i < numCircles; i++) {
+ final Cell cell = pattern.get(i);
+ drawLookup[cell.getRow()][cell.getColumn()] = true;
+ }
+
+ // figure out in progress portion of ghosting line
+
+ final boolean needToUpdateInProgressPoint = numCircles > 0
+ && numCircles < count;
+
+ if (needToUpdateInProgressPoint) {
+ final float percentageOfNextCircle =
+ ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
+ MILLIS_PER_CIRCLE_ANIMATING;
+
+ final Cell currentCell = pattern.get(numCircles - 1);
+ final float centerX = getCenterXForColumn(currentCell.column);
+ final float centerY = getCenterYForRow(currentCell.row);
+
+ final Cell nextCell = pattern.get(numCircles);
+ final float dx = percentageOfNextCircle *
+ (getCenterXForColumn(nextCell.column) - centerX);
+ final float dy = percentageOfNextCircle *
+ (getCenterYForRow(nextCell.row) - centerY);
+ mInProgressX = centerX + dx;
+ mInProgressY = centerY + dy;
+ }
+ // TODO: Infinite loop here...
+ invalidate();
+ }
+
+ final Path currentPath = mCurrentPath;
+ currentPath.rewind();
+
+ // draw the circles
+ for (int i = 0; i < 3; i++) {
+ float centerY = getCenterYForRow(i);
+ for (int j = 0; j < 3; j++) {
+ CellState cellState = mCellStates[i][j];
+ float centerX = getCenterXForColumn(j);
+ float translationY = cellState.translationY;
+
+ if (mUseLockPatternDrawable) {
+ drawCellDrawable(canvas, i, j, cellState.radius, drawLookup[i][j]);
+ } else {
+ if (isHardwareAccelerated() && cellState.hwAnimating) {
+ RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
+ recordingCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY,
+ cellState.hwRadius, cellState.hwPaint);
+ } else {
+ drawCircle(canvas, (int) centerX, (int) centerY + translationY,
+ cellState.radius, drawLookup[i][j], cellState.alpha);
+ }
+ }
+ }
+ }
+
+ // TODO: the path should be created and cached every time we hit-detect a cell
+ // only the last segment of the path should be computed here
+ // draw the path of the pattern (unless we are in stealth mode)
+ final boolean drawPath = !mInStealthMode;
+
+ if (drawPath) {
+ mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
+
+ boolean anyCircles = false;
+ float lastX = 0f;
+ float lastY = 0f;
+ long elapsedRealtime = SystemClock.elapsedRealtime();
+ for (int i = 0; i < count; i++) {
+ Cell cell = pattern.get(i);
+
+ // only draw the part of the pattern stored in
+ // the lookup table (this is only different in the case
+ // of animation).
+ if (!drawLookup[cell.row][cell.column]) {
+ break;
+ }
+ anyCircles = true;
+
+ if (mLineFadeStart[i] == 0) {
+ mLineFadeStart[i] = SystemClock.elapsedRealtime();
+ }
+
+ float centerX = getCenterXForColumn(cell.column);
+ float centerY = getCenterYForRow(cell.row);
+ if (i != 0) {
+ // Set this line segment to fade away animated.
+ int lineFadeVal = (int) Math.min((elapsedRealtime -
+ mLineFadeStart[i]) * LINE_FADE_ALPHA_MULTIPLIER, 255f);
+
+ CellState state = mCellStates[cell.row][cell.column];
+ currentPath.rewind();
+ currentPath.moveTo(lastX, lastY);
+ if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
+ currentPath.lineTo(state.lineEndX, state.lineEndY);
+ if (mFadePattern) {
+ mPathPaint.setAlpha((int) 255 - lineFadeVal );
+ } else {
+ mPathPaint.setAlpha(255);
+ }
+ } else {
+ currentPath.lineTo(centerX, centerY);
+ if (mFadePattern) {
+ mPathPaint.setAlpha((int) 255 - lineFadeVal );
+ } else {
+ mPathPaint.setAlpha(255);
+ }
+ }
+ canvas.drawPath(currentPath, mPathPaint);
+ }
+ lastX = centerX;
+ lastY = centerY;
+ }
+
+ // draw last in progress section
+ if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
+ && anyCircles) {
+ currentPath.rewind();
+ currentPath.moveTo(lastX, lastY);
+ currentPath.lineTo(mInProgressX, mInProgressY);
+
+ mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
+ mInProgressX, mInProgressY, lastX, lastY) * 255f));
+ canvas.drawPath(currentPath, mPathPaint);
+ }
+ }
+ }
+
+ private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
+ float diffX = x - lastX;
+ float diffY = y - lastY;
+ float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
+ float frac = dist/mSquareWidth;
+ return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
+ }
+
+ private int getCurrentColor(boolean partOfPattern) {
+ if (!partOfPattern || mInStealthMode || mPatternInProgress) {
+ // unselected circle
+ return mRegularColor;
+ } else if (mPatternDisplayMode == DisplayMode.Wrong) {
+ // the pattern is wrong
+ return mErrorColor;
+ } else if (mPatternDisplayMode == DisplayMode.Correct ||
+ mPatternDisplayMode == DisplayMode.Animate) {
+ return mSuccessColor;
+ } else {
+ throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
+ }
+ }
+
+ /**
+ * @param partOfPattern Whether this circle is part of the pattern.
+ */
+ private void drawCircle(Canvas canvas, float centerX, float centerY, float radius,
+ boolean partOfPattern, float alpha) {
+ mPaint.setColor(getCurrentColor(partOfPattern));
+ mPaint.setAlpha((int) (alpha * 255));
+ canvas.drawCircle(centerX, centerY, radius, mPaint);
+ }
+
+ /**
+ * @param partOfPattern Whether this circle is part of the pattern.
+ */
+ private void drawCellDrawable(Canvas canvas, int i, int j, float radius,
+ boolean partOfPattern) {
+ Rect dst = new Rect(
+ (int) (mPaddingLeft + j * mSquareWidth),
+ (int) (mPaddingTop + i * mSquareHeight),
+ (int) (mPaddingLeft + (j + 1) * mSquareWidth),
+ (int) (mPaddingTop + (i + 1) * mSquareHeight));
+ float scale = radius / (mDotSize / 2);
+
+ // Only draw on this square with the appropriate scale.
+ canvas.save();
+ canvas.clipRect(dst);
+ canvas.scale(scale, scale, dst.centerX(), dst.centerY());
+ if (!partOfPattern || scale > 1) {
+ mNotSelectedDrawable.draw(canvas);
+ } else {
+ mSelectedDrawable.draw(canvas);
+ }
+ canvas.restore();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ byte[] patternBytes = LockPatternUtils.patternToByteArray(mPattern);
+ String patternString = patternBytes != null ? new String(patternBytes) : null;
+ return new SavedState(superState,
+ patternString,
+ mPatternDisplayMode.ordinal(),
+ mInputEnabled, mInStealthMode, mEnableHapticFeedback);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ setPattern(
+ DisplayMode.Correct,
+ LockPatternUtils.byteArrayToPattern(ss.getSerializedPattern().getBytes()));
+ mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
+ mInputEnabled = ss.isInputEnabled();
+ mInStealthMode = ss.isInStealthMode();
+ mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
+ }
+
+ /**
+ * The parecelable for saving and restoring a lock pattern view.
+ */
+ private static class SavedState extends BaseSavedState {
+
+ private final String mSerializedPattern;
+ private final int mDisplayMode;
+ private final boolean mInputEnabled;
+ private final boolean mInStealthMode;
+ private final boolean mTactileFeedbackEnabled;
+
+ /**
+ * Constructor called from {@link LockPatternView#onSaveInstanceState()}
+ */
+ @UnsupportedAppUsage
+ private SavedState(Parcelable superState, String serializedPattern, int displayMode,
+ boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
+ super(superState);
+ mSerializedPattern = serializedPattern;
+ mDisplayMode = displayMode;
+ mInputEnabled = inputEnabled;
+ mInStealthMode = inStealthMode;
+ mTactileFeedbackEnabled = tactileFeedbackEnabled;
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ @UnsupportedAppUsage
+ private SavedState(Parcel in) {
+ super(in);
+ mSerializedPattern = in.readString();
+ mDisplayMode = in.readInt();
+ mInputEnabled = (Boolean) in.readValue(null);
+ mInStealthMode = (Boolean) in.readValue(null);
+ mTactileFeedbackEnabled = (Boolean) in.readValue(null);
+ }
+
+ public String getSerializedPattern() {
+ return mSerializedPattern;
+ }
+
+ public int getDisplayMode() {
+ return mDisplayMode;
+ }
+
+ public boolean isInputEnabled() {
+ return mInputEnabled;
+ }
+
+ public boolean isInStealthMode() {
+ return mInStealthMode;
+ }
+
+ public boolean isTactileFeedbackEnabled(){
+ return mTactileFeedbackEnabled;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeString(mSerializedPattern);
+ dest.writeInt(mDisplayMode);
+ dest.writeValue(mInputEnabled);
+ dest.writeValue(mInStealthMode);
+ dest.writeValue(mTactileFeedbackEnabled);
+ }
+
+ @SuppressWarnings({ "unused", "hiding" }) // Found using reflection
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
+ private Rect mTempRect = new Rect();
+ private final SparseArray<VirtualViewContainer> mItems = new SparseArray<>();
+
+ class VirtualViewContainer {
+ public VirtualViewContainer(CharSequence description) {
+ this.description = description;
+ }
+ CharSequence description;
+ };
+
+ public PatternExploreByTouchHelper(View forView) {
+ super(forView);
+ for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
+ mItems.put(i, new VirtualViewContainer(getTextForVirtualView(i)));
+ }
+ }
+
+ @Override
+ protected int getVirtualViewAt(float x, float y) {
+ // This must use the same hit logic for the screen to ensure consistency whether
+ // accessibility is on or off.
+ int id = getVirtualViewIdForHit(x, y);
+ return id;
+ }
+
+ @Override
+ protected void getVisibleVirtualViews(IntArray virtualViewIds) {
+ if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")");
+ if (!mPatternInProgress) {
+ return;
+ }
+ for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
+ // Add all views. As views are added to the pattern, we remove them
+ // from notification by making them non-clickable below.
+ virtualViewIds.add(i);
+ }
+ }
+
+ @Override
+ protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+ if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")");
+ // Announce this view
+ VirtualViewContainer container = mItems.get(virtualViewId);
+ if (container != null) {
+ event.getText().add(container.description);
+ }
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(host, event);
+ if (!mPatternInProgress) {
+ CharSequence contentDescription = getContext().getText(
+ com.android.internal.R.string.lockscreen_access_pattern_area);
+ event.setContentDescription(contentDescription);
+ }
+ }
+
+ @Override
+ protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
+ if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")");
+
+ // Node and event text and content descriptions are usually
+ // identical, so we'll use the exact same string as before.
+ node.setText(getTextForVirtualView(virtualViewId));
+ node.setContentDescription(getTextForVirtualView(virtualViewId));
+
+ if (mPatternInProgress) {
+ node.setFocusable(true);
+
+ if (isClickable(virtualViewId)) {
+ // Mark this node of interest by making it clickable.
+ node.addAction(AccessibilityAction.ACTION_CLICK);
+ node.setClickable(isClickable(virtualViewId));
+ }
+ }
+
+ // Compute bounds for this object
+ final Rect bounds = getBoundsForVirtualView(virtualViewId);
+ if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString());
+ node.setBoundsInParent(bounds);
+ }
+
+ private boolean isClickable(int virtualViewId) {
+ // Dots are clickable if they're not part of the current pattern.
+ if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
+ int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3;
+ int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3;
+ return !mPatternDrawLookup[row][col];
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+ Bundle arguments) {
+ if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId
+ + ", action=" + action);
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK:
+ // Click handling should be consistent with
+ // onTouchEvent(). This ensures that the view works the
+ // same whether accessibility is turned on or off.
+ return onItemClicked(virtualViewId);
+ default:
+ if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in "
+ + "onPerformActionForVirtualView(viewId="
+ + virtualViewId + "action=" + action + ")");
+ }
+ return false;
+ }
+
+ boolean onItemClicked(int index) {
+ if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")");
+
+ // Since the item's checked state is exposed to accessibility
+ // services through its AccessibilityNodeInfo, we need to invalidate
+ // the item's virtual view. At some point in the future, the
+ // framework will obtain an updated version of the virtual view.
+ invalidateVirtualView(index);
+
+ // We need to let the framework know what type of event
+ // happened. Accessibility services may use this event to provide
+ // appropriate feedback to the user.
+ sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+ return true;
+ }
+
+ private Rect getBoundsForVirtualView(int virtualViewId) {
+ int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID;
+ final Rect bounds = mTempRect;
+ final int row = ordinal / 3;
+ final int col = ordinal % 3;
+ final CellState cell = mCellStates[row][col];
+ float centerX = getCenterXForColumn(col);
+ float centerY = getCenterYForRow(row);
+ float cellheight = mSquareHeight * mHitFactor * 0.5f;
+ float cellwidth = mSquareWidth * mHitFactor * 0.5f;
+ bounds.left = (int) (centerX - cellwidth);
+ bounds.right = (int) (centerX + cellwidth);
+ bounds.top = (int) (centerY - cellheight);
+ bounds.bottom = (int) (centerY + cellheight);
+ return bounds;
+ }
+
+ private CharSequence getTextForVirtualView(int virtualViewId) {
+ final Resources res = getResources();
+ return res.getString(R.string.lockscreen_access_pattern_cell_added_verbose,
+ virtualViewId);
+ }
+
+ /**
+ * Helper method to find which cell a point maps to
+ *
+ * if there's no hit.
+ * @param x touch position x
+ * @param y touch position y
+ * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
+ */
+ private int getVirtualViewIdForHit(float x, float y) {
+ final int rowHit = getRowHit(y);
+ if (rowHit < 0) {
+ return ExploreByTouchHelper.INVALID_ID;
+ }
+ final int columnHit = getColumnHit(x);
+ if (columnHit < 0) {
+ return ExploreByTouchHelper.INVALID_ID;
+ }
+ boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
+ int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
+ int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
+ if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
+ + view + "avail =" + dotAvailable);
+ return view;
+ }
+ }
+}
diff --git a/com/android/internal/widget/LockScreenWidgetCallback.java b/com/android/internal/widget/LockScreenWidgetCallback.java
new file mode 100644
index 0000000..d7ad6c0
--- /dev/null
+++ b/com/android/internal/widget/LockScreenWidgetCallback.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+import android.view.View;
+
+/**
+ * An interface used by LockScreenWidgets to send messages to lock screen.
+ */
+public interface LockScreenWidgetCallback {
+ // Sends a message to lock screen requesting the given view be shown. May be ignored, depending
+ // on lock screen state. View must be the top-level lock screen widget or it will be ignored.
+ public void requestShow(View self);
+
+ // Sends a message to lock screen requesting the view to be hidden.
+ public void requestHide(View self);
+
+ // Whether or not this view is currently visible on LockScreen
+ public boolean isVisible(View self);
+
+ // Sends a message to lock screen that user has interacted with widget. This should be used
+ // exclusively in response to user activity, i.e. user hits a button in the view.
+ public void userActivity(View self);
+
+}
diff --git a/com/android/internal/widget/LockScreenWidgetInterface.java b/com/android/internal/widget/LockScreenWidgetInterface.java
new file mode 100644
index 0000000..8f80cfc
--- /dev/null
+++ b/com/android/internal/widget/LockScreenWidgetInterface.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+public interface LockScreenWidgetInterface {
+
+ public void setCallback(LockScreenWidgetCallback callback);
+
+ public boolean providesClock();
+
+}
diff --git a/com/android/internal/widget/LockSettingsInternal.java b/com/android/internal/widget/LockSettingsInternal.java
new file mode 100644
index 0000000..38588ea
--- /dev/null
+++ b/com/android/internal/widget/LockSettingsInternal.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.app.admin.PasswordMetrics;
+
+/**
+ * LockSettingsService local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class LockSettingsInternal {
+
+ /**
+ * Create an escrow token for the current user, which can later be used to unlock FBE
+ * or change user password.
+ *
+ * After adding, if the user currently has lockscreen password, he will need to perform a
+ * confirm credential operation in order to activate the token for future use.
+ * Once the token is activated, the callback that is passed here is called. If the user
+ * has no secure lockscreen, then the token is activated immediately.
+ *
+ * @return a unique 64-bit token handle which is needed to refer to this token later.
+ */
+ public abstract long addEscrowToken(byte[] token, int userId,
+ LockPatternUtils.EscrowTokenStateChangeCallback callback);
+
+ /**
+ * Remove an escrow token.
+ *
+ * @return true if the given handle refers to a valid token previously returned from
+ * {@link #addEscrowToken}, whether it's active or not. return false otherwise.
+ */
+ public abstract boolean removeEscrowToken(long handle, int userId);
+
+ /**
+ * Check if the given escrow token is active or not. Only active token can be used to call
+ * {@link #setLockCredentialWithToken} and {@link #unlockUserWithToken}
+ */
+ public abstract boolean isEscrowTokenActive(long handle, int userId);
+
+ /**
+ * Set the lock credential.
+ *
+ * @return true if password is set.
+ */
+ public abstract boolean setLockCredentialWithToken(LockscreenCredential credential,
+ long tokenHandle, byte[] token, int userId);
+
+ public abstract boolean unlockUserWithToken(long tokenHandle, byte[] token, int userId);
+
+ /**
+ * Returns PasswordMetrics object corresponding to the given user's lockscreen password.
+ * If the user has a password but its metrics isn't known yet (for example if the device
+ * has not been unlocked since boot), this method will return {@code null}.
+ * If the user has no password, a default PasswordMetrics (PASSWORD_QUALITY_UNSPECIFIED)
+ * will be returned.
+ *
+ * Calling this method on a managed profile user with unified challenge is undefined.
+ *
+ * @param userHandle the user for whom to provide metrics.
+ * @return the user password metrics.
+ */
+ public abstract @Nullable PasswordMetrics getUserPasswordMetrics(int userHandle);
+
+ /**
+ * Prepare for reboot escrow. This triggers the strong auth to be required. After the escrow
+ * is complete as indicated by calling to the listener registered with {@link
+ * #setRebootEscrowListener}, then {@link #armRebootEscrow()} should be called before
+ * rebooting to apply the update.
+ */
+ public abstract void prepareRebootEscrow();
+
+ /**
+ * Registers a listener for when the RebootEscrow HAL has stored its data needed for rebooting
+ * for an OTA.
+ *
+ * @see RebootEscrowListener
+ * @param listener
+ */
+ public abstract void setRebootEscrowListener(RebootEscrowListener listener);
+
+ /**
+ * Requests that any data needed for rebooting is cleared from the RebootEscrow HAL.
+ */
+ public abstract void clearRebootEscrow();
+
+ /**
+ * Should be called immediately before rebooting for an update. This depends on {@link
+ * #prepareRebootEscrow()} having been called and the escrow completing.
+ *
+ * @return true if the arming worked
+ */
+ public abstract boolean armRebootEscrow();
+
+
+ /**
+ * Refreshes pending strong auth timeout with the latest admin requirement set by device policy.
+ */
+ public abstract void refreshStrongAuthTimeout(int userId);
+}
diff --git a/com/android/internal/widget/LockscreenCredential.java b/com/android/internal/widget/LockscreenCredential.java
new file mode 100644
index 0000000..55f30fb
--- /dev/null
+++ b/com/android/internal/widget/LockscreenCredential.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2019 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.internal.widget;
+
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.storage.StorageManager;
+import android.text.TextUtils;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A class representing a lockscreen credential. It can be either an empty password, a pattern
+ * or a password (or PIN).
+ *
+ * <p> As required by some security certification, the framework tries its best to
+ * remove copies of the lockscreen credential bytes from memory. In this regard, this class
+ * abuses the {@link AutoCloseable} interface for sanitizing memory. This
+ * presents a nice syntax to auto-zeroize memory with the try-with-resource statement:
+ * <pre>
+ * try {LockscreenCredential credential = LockscreenCredential.createPassword(...) {
+ * // Process the credential in some way
+ * }
+ * </pre>
+ * With this construct, we can garantee that there will be no copies of the password left in
+ * memory when the credential goes out of scope. This should help mitigate certain class of
+ * attacks where the attcker gains read-only access to full device memory (cold boot attack,
+ * unsecured software/hardware memory dumping interfaces such as JTAG).
+ */
+public class LockscreenCredential implements Parcelable, AutoCloseable {
+
+ private final int mType;
+ // Stores raw credential bytes, or null if credential has been zeroized. An empty password
+ // is represented as a byte array of length 0.
+ private byte[] mCredential;
+
+ /**
+ * Private constructor, use static builder methods instead.
+ *
+ * <p> Builder methods should create a private copy of the credential bytes and pass in here.
+ * LockscreenCredential will only store the reference internally without copying. This is to
+ * minimize the number of extra copies introduced.
+ */
+ private LockscreenCredential(int type, byte[] credential) {
+ Objects.requireNonNull(credential);
+ if (type == CREDENTIAL_TYPE_NONE) {
+ Preconditions.checkArgument(credential.length == 0);
+ } else {
+ // Do not allow constructing a CREDENTIAL_TYPE_PASSWORD_OR_PIN object.
+ Preconditions.checkArgument(type == CREDENTIAL_TYPE_PIN
+ || type == CREDENTIAL_TYPE_PASSWORD
+ || type == CREDENTIAL_TYPE_PATTERN);
+ Preconditions.checkArgument(credential.length > 0);
+ }
+ mType = type;
+ mCredential = credential;
+ }
+
+ /**
+ * Creates a LockscreenCredential object representing empty password.
+ */
+ public static LockscreenCredential createNone() {
+ return new LockscreenCredential(CREDENTIAL_TYPE_NONE, new byte[0]);
+ }
+
+ /**
+ * Creates a LockscreenCredential object representing the given pattern.
+ */
+ public static LockscreenCredential createPattern(@NonNull List<LockPatternView.Cell> pattern) {
+ return new LockscreenCredential(CREDENTIAL_TYPE_PATTERN,
+ LockPatternUtils.patternToByteArray(pattern));
+ }
+
+ /**
+ * Creates a LockscreenCredential object representing the given alphabetic password.
+ */
+ public static LockscreenCredential createPassword(@NonNull CharSequence password) {
+ return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD,
+ charSequenceToByteArray(password));
+ }
+
+ /**
+ * Creates a LockscreenCredential object representing a managed password for profile with
+ * unified challenge. This credentiall will have type {@code CREDENTIAL_TYPE_PASSWORD} for now.
+ * TODO: consider add a new credential type for this. This can then supersede the
+ * isLockTiedToParent argument in various places in LSS.
+ */
+ public static LockscreenCredential createManagedPassword(@NonNull byte[] password) {
+ return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD,
+ Arrays.copyOf(password, password.length));
+ }
+
+ /**
+ * Creates a LockscreenCredential object representing the given numeric PIN.
+ */
+ public static LockscreenCredential createPin(@NonNull CharSequence pin) {
+ return new LockscreenCredential(CREDENTIAL_TYPE_PIN,
+ charSequenceToByteArray(pin));
+ }
+
+ /**
+ * Creates a LockscreenCredential object representing the given alphabetic password.
+ * If the supplied password is empty, create an empty credential object.
+ */
+ public static LockscreenCredential createPasswordOrNone(@Nullable CharSequence password) {
+ if (TextUtils.isEmpty(password)) {
+ return createNone();
+ } else {
+ return createPassword(password);
+ }
+ }
+
+ /**
+ * Creates a LockscreenCredential object representing the given numeric PIN.
+ * If the supplied password is empty, create an empty credential object.
+ */
+ public static LockscreenCredential createPinOrNone(@Nullable CharSequence pin) {
+ if (TextUtils.isEmpty(pin)) {
+ return createNone();
+ } else {
+ return createPin(pin);
+ }
+ }
+
+ private void ensureNotZeroized() {
+ Preconditions.checkState(mCredential != null, "Credential is already zeroized");
+ }
+ /**
+ * Returns the type of this credential. Can be one of {@link #CREDENTIAL_TYPE_NONE},
+ * {@link #CREDENTIAL_TYPE_PATTERN}, {@link #CREDENTIAL_TYPE_PIN} or
+ * {@link #CREDENTIAL_TYPE_PASSWORD}.
+ */
+ public int getType() {
+ ensureNotZeroized();
+ return mType;
+ }
+
+ /**
+ * Returns the credential bytes. This is a direct reference of the internal field so
+ * callers should not modify it.
+ *
+ */
+ public byte[] getCredential() {
+ ensureNotZeroized();
+ return mCredential;
+ }
+
+ /**
+ * Returns the credential type recognized by {@link StorageManager}. Can be one of
+ * {@link StorageManager#CRYPT_TYPE_DEFAULT}, {@link StorageManager#CRYPT_TYPE_PATTERN},
+ * {@link StorageManager#CRYPT_TYPE_PIN} or {@link StorageManager#CRYPT_TYPE_PASSWORD}.
+ */
+ public int getStorageCryptType() {
+ if (isNone()) {
+ return StorageManager.CRYPT_TYPE_DEFAULT;
+ }
+ if (isPattern()) {
+ return StorageManager.CRYPT_TYPE_PATTERN;
+ }
+ if (isPin()) {
+ return StorageManager.CRYPT_TYPE_PIN;
+ }
+ if (isPassword()) {
+ return StorageManager.CRYPT_TYPE_PASSWORD;
+ }
+ throw new IllegalStateException("Unhandled credential type");
+ }
+
+ /** Returns whether this is an empty credential */
+ public boolean isNone() {
+ ensureNotZeroized();
+ return mType == CREDENTIAL_TYPE_NONE;
+ }
+
+ /** Returns whether this is a pattern credential */
+ public boolean isPattern() {
+ ensureNotZeroized();
+ return mType == CREDENTIAL_TYPE_PATTERN;
+ }
+
+ /** Returns whether this is a numeric pin credential */
+ public boolean isPin() {
+ ensureNotZeroized();
+ return mType == CREDENTIAL_TYPE_PIN;
+ }
+
+ /** Returns whether this is an alphabetic password credential */
+ public boolean isPassword() {
+ ensureNotZeroized();
+ return mType == CREDENTIAL_TYPE_PASSWORD;
+ }
+
+ /** Returns the length of the credential */
+ public int size() {
+ ensureNotZeroized();
+ return mCredential.length;
+ }
+
+ /** Create a copy of the credential */
+ public LockscreenCredential duplicate() {
+ return new LockscreenCredential(mType,
+ mCredential != null ? Arrays.copyOf(mCredential, mCredential.length) : null);
+ }
+
+ /**
+ * Zeroize the credential bytes.
+ */
+ public void zeroize() {
+ if (mCredential != null) {
+ Arrays.fill(mCredential, (byte) 0);
+ mCredential = null;
+ }
+ }
+
+ /**
+ * Check if the credential meets minimal length requirement.
+ *
+ * @throws IllegalArgumentException if the credential is too short.
+ */
+ public void checkLength() {
+ if (isNone()) {
+ return;
+ }
+ if (isPattern()) {
+ if (size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) {
+ throw new IllegalArgumentException("pattern must not be null and at least "
+ + LockPatternUtils.MIN_LOCK_PATTERN_SIZE + " dots long.");
+ }
+ return;
+ }
+ if (isPassword() || isPin()) {
+ if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) {
+ throw new IllegalArgumentException("password must not be null and at least "
+ + "of length " + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE);
+ }
+ return;
+ }
+ }
+
+ /**
+ * Check if this credential's type matches one that's retrieved from disk. The nuance here is
+ * that the framework used to not distinguish between PIN and password, so this method will
+ * allow a PIN/Password LockscreenCredential to match against the legacy
+ * {@link #CREDENTIAL_TYPE_PASSWORD_OR_PIN} stored on disk.
+ */
+ public boolean checkAgainstStoredType(int storedCredentialType) {
+ if (storedCredentialType == CREDENTIAL_TYPE_PASSWORD_OR_PIN) {
+ return getType() == CREDENTIAL_TYPE_PASSWORD || getType() == CREDENTIAL_TYPE_PIN;
+ }
+ return getType() == storedCredentialType;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeByteArray(mCredential);
+ }
+
+ public static final Parcelable.Creator<LockscreenCredential> CREATOR =
+ new Parcelable.Creator<LockscreenCredential>() {
+
+ @Override
+ public LockscreenCredential createFromParcel(Parcel source) {
+ return new LockscreenCredential(source.readInt(), source.createByteArray());
+ }
+
+ @Override
+ public LockscreenCredential[] newArray(int size) {
+ return new LockscreenCredential[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void close() {
+ zeroize();
+ }
+
+ @Override
+ public int hashCode() {
+ // Effective Java — Always override hashCode when you override equals
+ return (17 + mType) * 31 + mCredential.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof LockscreenCredential)) return false;
+ final LockscreenCredential other = (LockscreenCredential) o;
+ return mType == other.mType && Arrays.equals(mCredential, other.mCredential);
+ }
+
+ /**
+ * Converts a CharSequence to a byte array without requiring a toString(), which creates an
+ * additional copy.
+ *
+ * @param chars The CharSequence to convert
+ * @return A byte array representing the input
+ */
+ private static byte[] charSequenceToByteArray(CharSequence chars) {
+ if (chars == null) {
+ return new byte[0];
+ }
+ byte[] bytes = new byte[chars.length()];
+ for (int i = 0; i < chars.length(); i++) {
+ bytes[i] = (byte) chars.charAt(i);
+ }
+ return bytes;
+ }
+}
diff --git a/com/android/internal/widget/MediaNotificationView.java b/com/android/internal/widget/MediaNotificationView.java
new file mode 100644
index 0000000..9bb4501
--- /dev/null
+++ b/com/android/internal/widget/MediaNotificationView.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+
+/**
+ * A TextView that can float around an image on the end.
+ *
+ * @hide
+ */
[email protected]
+public class MediaNotificationView extends FrameLayout {
+
+ private final int mNotificationContentMarginEnd;
+ private final int mNotificationContentImageMarginEnd;
+ private ImageView mRightIcon;
+ private View mActions;
+ private NotificationHeaderView mHeader;
+ private View mMainColumn;
+ private View mMediaContent;
+ private int mImagePushIn;
+ private ArrayList<VisibilityChangeListener> mListeners;
+
+ public MediaNotificationView(Context context) {
+ this(context, null);
+ }
+
+ public MediaNotificationView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MediaNotificationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ boolean hasIcon = mRightIcon.getVisibility() != GONE;
+ if (!hasIcon) {
+ resetHeaderIndention();
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int mode = MeasureSpec.getMode(widthMeasureSpec);
+ boolean reMeasure = false;
+ mImagePushIn = 0;
+ if (hasIcon && mode != MeasureSpec.UNSPECIFIED) {
+ int size = MeasureSpec.getSize(widthMeasureSpec);
+ size = size - mActions.getMeasuredWidth();
+ ViewGroup.MarginLayoutParams layoutParams =
+ (MarginLayoutParams) mRightIcon.getLayoutParams();
+ int imageEndMargin = layoutParams.getMarginEnd();
+ size -= imageEndMargin;
+ int fullHeight = mMediaContent.getMeasuredHeight();
+ if (size > fullHeight) {
+ size = fullHeight;
+ } else if (size < fullHeight) {
+ size = Math.max(0, size);
+ mImagePushIn = fullHeight - size;
+ }
+ if (layoutParams.width != fullHeight || layoutParams.height != fullHeight) {
+ layoutParams.width = fullHeight;
+ layoutParams.height = fullHeight;
+ mRightIcon.setLayoutParams(layoutParams);
+ reMeasure = true;
+ }
+
+ // lets ensure that the main column doesn't run into the image
+ ViewGroup.MarginLayoutParams params
+ = (MarginLayoutParams) mMainColumn.getLayoutParams();
+ int marginEnd = size + imageEndMargin + mNotificationContentMarginEnd;
+ if (marginEnd != params.getMarginEnd()) {
+ params.setMarginEnd(marginEnd);
+ mMainColumn.setLayoutParams(params);
+ reMeasure = true;
+ }
+ // margin for the entire header line
+ int headerMarginEnd = imageEndMargin;
+ // margin for the header text (not including the expand button and other icons)
+ int headerTextMarginEnd = size + imageEndMargin;
+ if (headerTextMarginEnd != mHeader.getHeaderTextMarginEnd()) {
+ mHeader.setHeaderTextMarginEnd(headerTextMarginEnd);
+ reMeasure = true;
+ }
+ params = (MarginLayoutParams) mHeader.getLayoutParams();
+ if (params.getMarginEnd() != headerMarginEnd) {
+ params.setMarginEnd(headerMarginEnd);
+ mHeader.setLayoutParams(params);
+ reMeasure = true;
+ }
+ if (mHeader.getPaddingEnd() != mNotificationContentImageMarginEnd) {
+ mHeader.setPaddingRelative(mHeader.getPaddingStart(),
+ mHeader.getPaddingTop(),
+ mNotificationContentImageMarginEnd,
+ mHeader.getPaddingBottom());
+ reMeasure = true;
+ }
+ }
+ if (reMeasure) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mImagePushIn > 0) {
+ if (this.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ mImagePushIn *= -1;
+ }
+ mRightIcon.layout(mRightIcon.getLeft() + mImagePushIn, mRightIcon.getTop(),
+ mRightIcon.getRight() + mImagePushIn, mRightIcon.getBottom());
+ }
+ }
+
+ private void resetHeaderIndention() {
+ if (mHeader.getPaddingEnd() != mNotificationContentMarginEnd) {
+ mHeader.setPaddingRelative(mHeader.getPaddingStart(),
+ mHeader.getPaddingTop(),
+ mNotificationContentMarginEnd,
+ mHeader.getPaddingBottom());
+ }
+ ViewGroup.MarginLayoutParams headerParams =
+ (MarginLayoutParams) mHeader.getLayoutParams();
+ headerParams.setMarginEnd(0);
+ if (headerParams.getMarginEnd() != 0) {
+ headerParams.setMarginEnd(0);
+ mHeader.setLayoutParams(headerParams);
+ }
+ }
+
+ public MediaNotificationView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mNotificationContentMarginEnd = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin_end);
+ mNotificationContentImageMarginEnd = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_image_margin_end);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mRightIcon = findViewById(com.android.internal.R.id.right_icon);
+ mActions = findViewById(com.android.internal.R.id.media_actions);
+ mHeader = findViewById(com.android.internal.R.id.notification_header);
+ mMainColumn = findViewById(com.android.internal.R.id.notification_main_column);
+ mMediaContent = findViewById(com.android.internal.R.id.notification_media_content);
+ }
+
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+ if (mListeners != null) {
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).onAggregatedVisibilityChanged(isVisible);
+ }
+ }
+ }
+
+ /**
+ * Add a listener to receive updates on the visibility of this view
+ *
+ * @param listener The listener to add.
+ */
+ public void addVisibilityListener(VisibilityChangeListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<>();
+ }
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+
+ /**
+ * Remove the specified listener
+ *
+ * @param listener The listener to remove.
+ */
+ public void removeVisibilityListener(VisibilityChangeListener listener) {
+ if (mListeners != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Interface for receiving updates when the view's visibility changes
+ */
+ public interface VisibilityChangeListener {
+ /**
+ * Method called when the visibility of this view has changed
+ * @param isVisible true if the view is now visible
+ */
+ void onAggregatedVisibilityChanged(boolean isVisible);
+ }
+}
diff --git a/com/android/internal/widget/MessagingGroup.java b/com/android/internal/widget/MessagingGroup.java
new file mode 100644
index 0000000..53272f7
--- /dev/null
+++ b/com/android/internal/widget/MessagingGroup.java
@@ -0,0 +1,706 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Person;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Pools;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A message of a {@link MessagingLayout}.
+ */
[email protected]
+public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild {
+ private static Pools.SimplePool<MessagingGroup> sInstancePool
+ = new Pools.SynchronizedPool<>(10);
+
+ /**
+ * Images are displayed inline.
+ */
+ public static final int IMAGE_DISPLAY_LOCATION_INLINE = 0;
+
+ /**
+ * Images are displayed at the end of the group.
+ */
+ public static final int IMAGE_DISPLAY_LOCATION_AT_END = 1;
+
+ /**
+ * Images are displayed externally.
+ */
+ public static final int IMAGE_DISPLAY_LOCATION_EXTERNAL = 2;
+
+
+ private MessagingLinearLayout mMessageContainer;
+ ImageFloatingTextView mSenderView;
+ private ImageView mAvatarView;
+ private View mAvatarContainer;
+ private String mAvatarSymbol = "";
+ private int mLayoutColor;
+ private CharSequence mAvatarName = "";
+ private Icon mAvatarIcon;
+ private int mTextColor;
+ private int mSendingTextColor;
+ private List<MessagingMessage> mMessages;
+ private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>();
+ private boolean mFirstLayout;
+ private boolean mIsHidingAnimated;
+ private boolean mNeedsGeneratedAvatar;
+ private Person mSender;
+ private @ImageDisplayLocation int mImageDisplayLocation;
+ private ViewGroup mImageContainer;
+ private MessagingImageMessage mIsolatedMessage;
+ private boolean mClippingDisabled;
+ private Point mDisplaySize = new Point();
+ private ProgressBar mSendingSpinner;
+ private View mSendingSpinnerContainer;
+ private boolean mShowingAvatar = true;
+ private CharSequence mSenderName;
+ private boolean mSingleLine = false;
+ private LinearLayout mContentContainer;
+ private int mRequestedMaxDisplayedLines = Integer.MAX_VALUE;
+ private int mSenderTextPaddingSingleLine;
+ private boolean mIsFirstGroupInLayout = true;
+ private boolean mCanHideSenderIfFirst;
+ private boolean mIsInConversation = true;
+ private ViewGroup mMessagingIconContainer;
+ private int mConversationContentStart;
+ private int mNonConversationMarginEnd;
+
+ public MessagingGroup(@NonNull Context context) {
+ super(context);
+ }
+
+ public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mMessageContainer = findViewById(R.id.group_message_container);
+ mSenderView = findViewById(R.id.message_name);
+ mAvatarView = findViewById(R.id.message_icon);
+ mImageContainer = findViewById(R.id.messaging_group_icon_container);
+ mSendingSpinner = findViewById(R.id.messaging_group_sending_progress);
+ mMessagingIconContainer = findViewById(R.id.message_icon_container);
+ mContentContainer = findViewById(R.id.messaging_group_content_container);
+ mSendingSpinnerContainer = findViewById(R.id.messaging_group_sending_progress_container);
+ DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ mDisplaySize.x = displayMetrics.widthPixels;
+ mDisplaySize.y = displayMetrics.heightPixels;
+ mSenderTextPaddingSingleLine = getResources().getDimensionPixelSize(
+ R.dimen.messaging_group_singleline_sender_padding_end);
+ mConversationContentStart = getResources().getDimensionPixelSize(
+ R.dimen.conversation_content_start);
+ mNonConversationMarginEnd = getResources().getDimensionPixelSize(
+ R.dimen.messaging_layout_margin_end);
+ }
+
+ public void updateClipRect() {
+ // We want to clip to the senderName if it's available, otherwise our images will come
+ // from a weird position
+ Rect clipRect;
+ if (mSenderView.getVisibility() != View.GONE && !mClippingDisabled) {
+ int top;
+ if (mSingleLine) {
+ top = 0;
+ } else {
+ top = getDistanceFromParent(mSenderView, mContentContainer)
+ - getDistanceFromParent(mMessageContainer, mContentContainer)
+ + mSenderView.getHeight();
+ }
+ int size = Math.max(mDisplaySize.x, mDisplaySize.y);
+ clipRect = new Rect(-size, top, size, size);
+ } else {
+ clipRect = null;
+ }
+ mMessageContainer.setClipBounds(clipRect);
+ }
+
+ private int getDistanceFromParent(View searchedView, ViewGroup parent) {
+ int position = 0;
+ View view = searchedView;
+ while(view != parent) {
+ position += view.getTop() + view.getTranslationY();
+ view = (View) view.getParent();
+ }
+ return position;
+ }
+
+ public void setSender(Person sender, CharSequence nameOverride) {
+ mSender = sender;
+ if (nameOverride == null) {
+ nameOverride = sender.getName();
+ }
+ mSenderName = nameOverride;
+ if (mSingleLine && !TextUtils.isEmpty(nameOverride)) {
+ nameOverride = mContext.getResources().getString(
+ R.string.conversation_single_line_name_display, nameOverride);
+ }
+ mSenderView.setText(nameOverride);
+ mNeedsGeneratedAvatar = sender.getIcon() == null;
+ if (!mNeedsGeneratedAvatar) {
+ setAvatar(sender.getIcon());
+ }
+ updateSenderVisibility();
+ }
+
+ /**
+ * Should the avatar be shown for this view.
+ *
+ * @param showingAvatar should it be shown
+ */
+ public void setShowingAvatar(boolean showingAvatar) {
+ mAvatarView.setVisibility(showingAvatar ? VISIBLE : GONE);
+ mShowingAvatar = showingAvatar;
+ }
+
+ public void setSending(boolean sending) {
+ int visibility = sending ? VISIBLE : GONE;
+ if (mSendingSpinnerContainer.getVisibility() != visibility) {
+ mSendingSpinnerContainer.setVisibility(visibility);
+ updateMessageColor();
+ }
+ }
+
+ private int calculateSendingTextColor() {
+ TypedValue alphaValue = new TypedValue();
+ mContext.getResources().getValue(
+ R.dimen.notification_secondary_text_disabled_alpha, alphaValue, true);
+ float alpha = alphaValue.getFloat();
+ return Color.valueOf(
+ Color.red(mTextColor),
+ Color.green(mTextColor),
+ Color.blue(mTextColor),
+ alpha).toArgb();
+ }
+
+ public void setAvatar(Icon icon) {
+ mAvatarIcon = icon;
+ if (mShowingAvatar || icon == null) {
+ mAvatarView.setImageIcon(icon);
+ }
+ mAvatarSymbol = "";
+ mAvatarName = "";
+ }
+
+ static MessagingGroup createGroup(MessagingLinearLayout layout) {;
+ MessagingGroup createdGroup = sInstancePool.acquire();
+ if (createdGroup == null) {
+ createdGroup = (MessagingGroup) LayoutInflater.from(layout.getContext()).inflate(
+ R.layout.notification_template_messaging_group, layout,
+ false);
+ createdGroup.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
+ }
+ layout.addView(createdGroup);
+ return createdGroup;
+ }
+
+ public void removeMessage(MessagingMessage messagingMessage) {
+ View view = messagingMessage.getView();
+ boolean wasShown = view.isShown();
+ ViewGroup messageParent = (ViewGroup) view.getParent();
+ if (messageParent == null) {
+ return;
+ }
+ messageParent.removeView(view);
+ Runnable recycleRunnable = () -> {
+ messageParent.removeTransientView(view);
+ messagingMessage.recycle();
+ };
+ if (wasShown && !MessagingLinearLayout.isGone(view)) {
+ messageParent.addTransientView(view, 0);
+ performRemoveAnimation(view, recycleRunnable);
+ } else {
+ recycleRunnable.run();
+ }
+ }
+
+ public void recycle() {
+ if (mIsolatedMessage != null) {
+ mImageContainer.removeView(mIsolatedMessage);
+ }
+ for (int i = 0; i < mMessages.size(); i++) {
+ MessagingMessage message = mMessages.get(i);
+ mMessageContainer.removeView(message.getView());
+ message.recycle();
+ }
+ setAvatar(null);
+ mAvatarView.setAlpha(1.0f);
+ mAvatarView.setTranslationY(0.0f);
+ mSenderView.setAlpha(1.0f);
+ mSenderView.setTranslationY(0.0f);
+ setAlpha(1.0f);
+ mIsolatedMessage = null;
+ mMessages = null;
+ mSenderName = null;
+ mAddedMessages.clear();
+ mFirstLayout = true;
+ setCanHideSenderIfFirst(false);
+ setIsFirstInLayout(true);
+
+ setMaxDisplayedLines(Integer.MAX_VALUE);
+ setSingleLine(false);
+ setShowingAvatar(true);
+ MessagingPropertyAnimator.recycle(this);
+ sInstancePool.release(MessagingGroup.this);
+ }
+
+ public void removeGroupAnimated(Runnable endAction) {
+ performRemoveAnimation(this, () -> {
+ setAlpha(1.0f);
+ MessagingPropertyAnimator.setToLaidOutPosition(this);
+ if (endAction != null) {
+ endAction.run();
+ }
+ });
+ }
+
+ public void performRemoveAnimation(View message, Runnable endAction) {
+ performRemoveAnimation(message, -message.getHeight(), endAction);
+ }
+
+ private void performRemoveAnimation(View view, int disappearTranslation, Runnable endAction) {
+ MessagingPropertyAnimator.startLocalTranslationTo(view, disappearTranslation,
+ MessagingLayout.FAST_OUT_LINEAR_IN);
+ MessagingPropertyAnimator.fadeOut(view, endAction);
+ }
+
+ public CharSequence getSenderName() {
+ return mSenderName;
+ }
+
+ public static void dropCache() {
+ sInstancePool = new Pools.SynchronizedPool<>(10);
+ }
+
+ @Override
+ public int getMeasuredType() {
+ if (mIsolatedMessage != null) {
+ // We only want to show one group if we have an inline image, so let's return shortened
+ // to avoid displaying the other ones.
+ return MEASURED_SHORTENED;
+ }
+ boolean hasNormal = false;
+ for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) {
+ View child = mMessageContainer.getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ if (child instanceof MessagingLinearLayout.MessagingChild) {
+ int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType();
+ boolean tooSmall = type == MEASURED_TOO_SMALL;
+ final MessagingLinearLayout.LayoutParams lp =
+ (MessagingLinearLayout.LayoutParams) child.getLayoutParams();
+ tooSmall |= lp.hide;
+ if (tooSmall) {
+ if (hasNormal) {
+ return MEASURED_SHORTENED;
+ } else {
+ return MEASURED_TOO_SMALL;
+ }
+ } else if (type == MEASURED_SHORTENED) {
+ return MEASURED_SHORTENED;
+ } else {
+ hasNormal = true;
+ }
+ }
+ }
+ return MEASURED_NORMAL;
+ }
+
+ @Override
+ public int getConsumedLines() {
+ int result = 0;
+ for (int i = 0; i < mMessageContainer.getChildCount(); i++) {
+ View child = mMessageContainer.getChildAt(i);
+ if (child instanceof MessagingLinearLayout.MessagingChild) {
+ result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines();
+ }
+ }
+ result = mIsolatedMessage != null ? Math.max(result, 1) : result;
+ // A group is usually taking up quite some space with the padding and the name, let's add 1
+ return result + 1;
+ }
+
+ @Override
+ public void setMaxDisplayedLines(int lines) {
+ mRequestedMaxDisplayedLines = lines;
+ updateMaxDisplayedLines();
+ }
+
+ private void updateMaxDisplayedLines() {
+ mMessageContainer.setMaxDisplayedLines(mSingleLine ? 1 : mRequestedMaxDisplayedLines);
+ }
+
+ @Override
+ public void hideAnimated() {
+ setIsHidingAnimated(true);
+ removeGroupAnimated(() -> setIsHidingAnimated(false));
+ }
+
+ @Override
+ public boolean isHidingAnimated() {
+ return mIsHidingAnimated;
+ }
+
+ @Override
+ public void setIsFirstInLayout(boolean first) {
+ if (first != mIsFirstGroupInLayout) {
+ mIsFirstGroupInLayout = first;
+ updateSenderVisibility();
+ }
+ }
+
+ /**
+ * @param canHide true if the sender can be hidden if it is first
+ */
+ public void setCanHideSenderIfFirst(boolean canHide) {
+ if (mCanHideSenderIfFirst != canHide) {
+ mCanHideSenderIfFirst = canHide;
+ updateSenderVisibility();
+ }
+ }
+
+ private void updateSenderVisibility() {
+ boolean hidden = (mIsFirstGroupInLayout || mSingleLine) && mCanHideSenderIfFirst
+ || TextUtils.isEmpty(mSenderName);
+ mSenderView.setVisibility(hidden ? GONE : VISIBLE);
+ }
+
+ @Override
+ public boolean hasDifferentHeightWhenFirst() {
+ return mCanHideSenderIfFirst && !mSingleLine && !TextUtils.isEmpty(mSenderName);
+ }
+
+ private void setIsHidingAnimated(boolean isHiding) {
+ ViewParent parent = getParent();
+ mIsHidingAnimated = isHiding;
+ invalidate();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).invalidate();
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol,
+ int layoutColor) {
+ if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol)
+ && layoutColor == mLayoutColor) {
+ return mAvatarIcon;
+ }
+ return null;
+ }
+
+ public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol,
+ int layoutColor) {
+ if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol)
+ || layoutColor != mLayoutColor) {
+ setAvatar(cachedIcon);
+ mAvatarSymbol = avatarSymbol;
+ setLayoutColor(layoutColor);
+ mAvatarName = avatarName;
+ }
+ }
+
+ public void setTextColors(int senderTextColor, int messageTextColor) {
+ mTextColor = messageTextColor;
+ mSendingTextColor = calculateSendingTextColor();
+ updateMessageColor();
+ mSenderView.setTextColor(senderTextColor);
+ }
+
+ public void setLayoutColor(int layoutColor) {
+ if (layoutColor != mLayoutColor){
+ mLayoutColor = layoutColor;
+ mSendingSpinner.setIndeterminateTintList(ColorStateList.valueOf(mLayoutColor));
+ }
+ }
+
+ private void updateMessageColor() {
+ if (mMessages != null) {
+ int color = mSendingSpinnerContainer.getVisibility() == View.VISIBLE
+ ? mSendingTextColor : mTextColor;
+ for (MessagingMessage message : mMessages) {
+ message.setColor(message.getMessage().isRemoteInputHistory() ? color : mTextColor);
+ }
+ }
+ }
+
+ public void setMessages(List<MessagingMessage> group) {
+ // Let's now make sure all children are added and in the correct order
+ int textMessageIndex = 0;
+ MessagingImageMessage isolatedMessage = null;
+ for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) {
+ MessagingMessage message = group.get(messageIndex);
+ if (message.getGroup() != this) {
+ message.setMessagingGroup(this);
+ mAddedMessages.add(message);
+ }
+ boolean isImage = message instanceof MessagingImageMessage;
+ if (mImageDisplayLocation != IMAGE_DISPLAY_LOCATION_INLINE && isImage) {
+ isolatedMessage = (MessagingImageMessage) message;
+ } else {
+ if (removeFromParentIfDifferent(message, mMessageContainer)) {
+ ViewGroup.LayoutParams layoutParams = message.getView().getLayoutParams();
+ if (layoutParams != null
+ && !(layoutParams instanceof MessagingLinearLayout.LayoutParams)) {
+ message.getView().setLayoutParams(
+ mMessageContainer.generateDefaultLayoutParams());
+ }
+ mMessageContainer.addView(message.getView(), textMessageIndex);
+ }
+ if (isImage) {
+ ((MessagingImageMessage) message).setIsolated(false);
+ }
+ // Let's sort them properly
+ if (textMessageIndex != mMessageContainer.indexOfChild(message.getView())) {
+ mMessageContainer.removeView(message.getView());
+ mMessageContainer.addView(message.getView(), textMessageIndex);
+ }
+ textMessageIndex++;
+ }
+ }
+ if (isolatedMessage != null) {
+ if (mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_AT_END
+ && removeFromParentIfDifferent(isolatedMessage, mImageContainer)) {
+ mImageContainer.removeAllViews();
+ mImageContainer.addView(isolatedMessage.getView());
+ } else if (mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_EXTERNAL) {
+ mImageContainer.removeAllViews();
+ }
+ isolatedMessage.setIsolated(true);
+ } else if (mIsolatedMessage != null) {
+ mImageContainer.removeAllViews();
+ }
+ mIsolatedMessage = isolatedMessage;
+ updateImageContainerVisibility();
+ mMessages = group;
+ updateMessageColor();
+ }
+
+ private void updateImageContainerVisibility() {
+ mImageContainer.setVisibility(mIsolatedMessage != null
+ && mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_AT_END
+ ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Remove the message from the parent if the parent isn't the one provided
+ * @return whether the message was removed
+ */
+ private boolean removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent) {
+ ViewParent parent = message.getView().getParent();
+ if (parent != newParent) {
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(message.getView());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (!mAddedMessages.isEmpty()) {
+ final boolean firstLayout = mFirstLayout;
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ for (MessagingMessage message : mAddedMessages) {
+ if (!message.getView().isShown()) {
+ continue;
+ }
+ MessagingPropertyAnimator.fadeIn(message.getView());
+ if (!firstLayout) {
+ MessagingPropertyAnimator.startLocalTranslationFrom(message.getView(),
+ message.getView().getHeight(),
+ MessagingLayout.LINEAR_OUT_SLOW_IN);
+ }
+ }
+ mAddedMessages.clear();
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ });
+ }
+ mFirstLayout = false;
+ updateClipRect();
+ }
+
+ /**
+ * Calculates the group compatibility between this and another group.
+ *
+ * @param otherGroup the other group to compare it with
+ *
+ * @return 0 if the groups are totally incompatible or 1 + the number of matching messages if
+ * they match.
+ */
+ public int calculateGroupCompatibility(MessagingGroup otherGroup) {
+ if (TextUtils.equals(getSenderName(),otherGroup.getSenderName())) {
+ int result = 1;
+ for (int i = 0; i < mMessages.size() && i < otherGroup.mMessages.size(); i++) {
+ MessagingMessage ownMessage = mMessages.get(mMessages.size() - 1 - i);
+ MessagingMessage otherMessage = otherGroup.mMessages.get(
+ otherGroup.mMessages.size() - 1 - i);
+ if (!ownMessage.sameAs(otherMessage)) {
+ return result;
+ }
+ result++;
+ }
+ return result;
+ }
+ return 0;
+ }
+
+ public View getSenderView() {
+ return mSenderView;
+ }
+
+ public View getAvatar() {
+ return mAvatarView;
+ }
+
+ public Icon getAvatarIcon() {
+ return mAvatarIcon;
+ }
+
+ public MessagingLinearLayout getMessageContainer() {
+ return mMessageContainer;
+ }
+
+ public MessagingImageMessage getIsolatedMessage() {
+ return mIsolatedMessage;
+ }
+
+ public boolean needsGeneratedAvatar() {
+ return mNeedsGeneratedAvatar;
+ }
+
+ public Person getSender() {
+ return mSender;
+ }
+
+ public void setClippingDisabled(boolean disabled) {
+ mClippingDisabled = disabled;
+ }
+
+ public void setImageDisplayLocation(@ImageDisplayLocation int displayLocation) {
+ if (mImageDisplayLocation != displayLocation) {
+ mImageDisplayLocation = displayLocation;
+ updateImageContainerVisibility();
+ }
+ }
+
+ public List<MessagingMessage> getMessages() {
+ return mMessages;
+ }
+
+ /**
+ * Set this layout to be single line and therefore displaying both the sender and the text on
+ * the same line.
+ *
+ * @param singleLine should be layout be single line
+ */
+ public void setSingleLine(boolean singleLine) {
+ if (singleLine != mSingleLine) {
+ mSingleLine = singleLine;
+ mContentContainer.setOrientation(
+ singleLine ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
+ MarginLayoutParams layoutParams = (MarginLayoutParams) mSenderView.getLayoutParams();
+ layoutParams.setMarginEnd(singleLine ? mSenderTextPaddingSingleLine : 0);
+ updateMaxDisplayedLines();
+ updateClipRect();
+ updateSenderVisibility();
+ }
+ }
+
+ public boolean isSingleLine() {
+ return mSingleLine;
+ }
+
+ /**
+ * Set this group to be displayed in a conversation and adjust the visual appearance
+ *
+ * @param isInConversation is this in a conversation
+ */
+ public void setIsInConversation(boolean isInConversation) {
+ if (mIsInConversation != isInConversation) {
+ mIsInConversation = isInConversation;
+ MarginLayoutParams layoutParams =
+ (MarginLayoutParams) mMessagingIconContainer.getLayoutParams();
+ layoutParams.width = mIsInConversation ? mConversationContentStart
+ : ViewPager.LayoutParams.WRAP_CONTENT;
+ layoutParams.setMarginEnd(mIsInConversation ? 0 : mNonConversationMarginEnd);
+ mMessagingIconContainer.setLayoutParams(layoutParams);
+ }
+ }
+
+ @IntDef(prefix = {"IMAGE_DISPLAY_LOCATION_"}, value = {
+ IMAGE_DISPLAY_LOCATION_INLINE,
+ IMAGE_DISPLAY_LOCATION_AT_END,
+ IMAGE_DISPLAY_LOCATION_EXTERNAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ImageDisplayLocation {
+ }
+}
diff --git a/com/android/internal/widget/MessagingImageMessage.java b/com/android/internal/widget/MessagingImageMessage.java
new file mode 100644
index 0000000..27689d4
--- /dev/null
+++ b/com/android/internal/widget/MessagingImageMessage.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pools;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+import java.io.IOException;
+
+/**
+ * A message of a {@link MessagingLayout} that is an image.
+ */
[email protected]
+public class MessagingImageMessage extends ImageView implements MessagingMessage {
+ private static final String TAG = "MessagingImageMessage";
+ private static Pools.SimplePool<MessagingImageMessage> sInstancePool
+ = new Pools.SynchronizedPool<>(10);
+ private final MessagingMessageState mState = new MessagingMessageState(this);
+ private final int mMinImageHeight;
+ private final Path mPath = new Path();
+ private final int mImageRounding;
+ private final int mMaxImageHeight;
+ private final int mIsolatedSize;
+ private final int mExtraSpacing;
+ private Drawable mDrawable;
+ private float mAspectRatio;
+ private int mActualWidth;
+ private int mActualHeight;
+ private boolean mIsIsolated;
+ private ImageResolver mImageResolver;
+
+ public MessagingImageMessage(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public MessagingImageMessage(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MessagingImageMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public MessagingImageMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mMinImageHeight = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.messaging_image_min_size);
+ mMaxImageHeight = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.messaging_image_max_height);
+ mImageRounding = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.messaging_image_rounding);
+ mExtraSpacing = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.messaging_image_extra_spacing);
+ setMaxHeight(mMaxImageHeight);
+ mIsolatedSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
+ }
+
+ @Override
+ public MessagingMessageState getState() {
+ return mState;
+ }
+
+ @Override
+ public boolean setMessage(Notification.MessagingStyle.Message message) {
+ MessagingMessage.super.setMessage(message);
+ Drawable drawable;
+ try {
+ Uri uri = message.getDataUri();
+ drawable = mImageResolver != null ? mImageResolver.loadImage(uri) :
+ LocalImageResolver.resolveImage(uri, getContext());
+ } catch (IOException | SecurityException e) {
+ e.printStackTrace();
+ return false;
+ }
+ if (drawable == null) {
+ return false;
+ }
+ int intrinsicHeight = drawable.getIntrinsicHeight();
+ if (intrinsicHeight == 0) {
+ Log.w(TAG, "Drawable with 0 intrinsic height was returned");
+ return false;
+ }
+ mDrawable = drawable;
+ mAspectRatio = ((float) mDrawable.getIntrinsicWidth()) / intrinsicHeight;
+ setImageDrawable(drawable);
+ setContentDescription(message.getText());
+ return true;
+ }
+
+ static MessagingMessage createMessage(IMessagingLayout layout,
+ Notification.MessagingStyle.Message m, ImageResolver resolver) {
+ MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout();
+ MessagingImageMessage createdMessage = sInstancePool.acquire();
+ if (createdMessage == null) {
+ createdMessage = (MessagingImageMessage) LayoutInflater.from(
+ layout.getContext()).inflate(
+ R.layout.notification_template_messaging_image_message,
+ messagingLinearLayout,
+ false);
+ createdMessage.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
+ }
+ createdMessage.setImageResolver(resolver);
+ boolean created = createdMessage.setMessage(m);
+ if (!created) {
+ createdMessage.recycle();
+ return MessagingTextMessage.createMessage(layout, m);
+ }
+ return createdMessage;
+ }
+
+ private void setImageResolver(ImageResolver resolver) {
+ mImageResolver = resolver;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.save();
+ canvas.clipPath(getRoundedRectPath());
+ // Calculate the right sizing ensuring that the image is nicely centered in the layout
+ // during transitions
+ int width = (int) Math.max((Math.min(getHeight(), getActualHeight()) * mAspectRatio),
+ getActualWidth());
+ int height = (int) Math.max((Math.min(getWidth(), getActualWidth()) / mAspectRatio),
+ getActualHeight());
+ height = (int) Math.max(height, width / mAspectRatio);
+ int left = (int) ((getActualWidth() - width) / 2.0f);
+ int top = (int) ((getActualHeight() - height) / 2.0f);
+ mDrawable.setBounds(left, top, left + width, top + height);
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+
+ public Path getRoundedRectPath() {
+ int left = 0;
+ int right = getActualWidth();
+ int top = 0;
+ int bottom = getActualHeight();
+ mPath.reset();
+ int width = right - left;
+ float roundnessX = mImageRounding;
+ float roundnessY = mImageRounding;
+ roundnessX = Math.min(width / 2, roundnessX);
+ roundnessY = Math.min((bottom - top) / 2, roundnessY);
+ mPath.moveTo(left, top + roundnessY);
+ mPath.quadTo(left, top, left + roundnessX, top);
+ mPath.lineTo(right - roundnessX, top);
+ mPath.quadTo(right, top, right, top + roundnessY);
+ mPath.lineTo(right, bottom - roundnessY);
+ mPath.quadTo(right, bottom, right - roundnessX, bottom);
+ mPath.lineTo(left + roundnessX, bottom);
+ mPath.quadTo(left, bottom, left, bottom - roundnessY);
+ mPath.close();
+ return mPath;
+ }
+
+ public void recycle() {
+ MessagingMessage.super.recycle();
+ setImageBitmap(null);
+ mDrawable = null;
+ sInstancePool.release(this);
+ }
+
+ public static void dropCache() {
+ sInstancePool = new Pools.SynchronizedPool<>(10);
+ }
+
+ @Override
+ public int getMeasuredType() {
+ int measuredHeight = getMeasuredHeight();
+ int minImageHeight;
+ if (mIsIsolated) {
+ minImageHeight = mIsolatedSize;
+ } else {
+ minImageHeight = mMinImageHeight;
+ }
+ boolean measuredTooSmall = measuredHeight < minImageHeight
+ && measuredHeight != mDrawable.getIntrinsicHeight();
+ if (measuredTooSmall) {
+ return MEASURED_TOO_SMALL;
+ } else {
+ if (!mIsIsolated && measuredHeight != mDrawable.getIntrinsicHeight()) {
+ return MEASURED_SHORTENED;
+ } else {
+ return MEASURED_NORMAL;
+ }
+ }
+ }
+
+ @Override
+ public void setMaxDisplayedLines(int lines) {
+ // Nothing to do, this should be handled automatically.
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mIsIsolated) {
+ // When isolated we have a fixed size, let's use that sizing.
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ } else {
+ // If we are displaying inline, we never want to go wider than actual size of the
+ // image, otherwise it will look quite blurry.
+ int width = Math.min(MeasureSpec.getSize(widthMeasureSpec),
+ mDrawable.getIntrinsicWidth());
+ int height = (int) Math.min(MeasureSpec.getSize(heightMeasureSpec), width
+ / mAspectRatio);
+ setMeasuredDimension(width, height);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ // TODO: ensure that this isn't called when transforming
+ setActualWidth(getWidth());
+ setActualHeight(getHeight());
+ }
+
+ @Override
+ public int getConsumedLines() {
+ return 3;
+ }
+
+ public void setActualWidth(int actualWidth) {
+ mActualWidth = actualWidth;
+ invalidate();
+ }
+
+ public int getActualWidth() {
+ return mActualWidth;
+ }
+
+ public void setActualHeight(int actualHeight) {
+ mActualHeight = actualHeight;
+ invalidate();
+ }
+
+ public int getActualHeight() {
+ return mActualHeight;
+ }
+
+ public void setIsolated(boolean isolated) {
+ if (mIsIsolated != isolated) {
+ mIsIsolated = isolated;
+ // update the layout params not to have margins
+ ViewGroup.MarginLayoutParams layoutParams =
+ (ViewGroup.MarginLayoutParams) getLayoutParams();
+ layoutParams.topMargin = isolated ? 0 : mExtraSpacing;
+ setLayoutParams(layoutParams);
+ }
+ }
+
+ @Override
+ public int getExtraSpacing() {
+ return mExtraSpacing;
+ }
+}
diff --git a/com/android/internal/widget/MessagingLayout.java b/com/android/internal/widget/MessagingLayout.java
new file mode 100644
index 0000000..a162e4e
--- /dev/null
+++ b/com/android/internal/widget/MessagingLayout.java
@@ -0,0 +1,612 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_AT_END;
+import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.app.Person;
+import android.app.RemoteInputHistoryItem;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.RemotableViewMethod;
+import android.view.ViewTreeObserver;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.util.ContrastColorUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+
+/**
+ * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
+ * messages and adapts the layout accordingly.
+ */
[email protected]
+public class MessagingLayout extends FrameLayout
+ implements ImageMessageConsumer, IMessagingLayout {
+
+ private static final float COLOR_SHIFT_AMOUNT = 60;
+ /**
+ * Pattren for filter some ingonable characters.
+ * p{Z} for any kind of whitespace or invisible separator.
+ * p{C} for any kind of punctuation character.
+ */
+ private static final Pattern IGNORABLE_CHAR_PATTERN
+ = Pattern.compile("[\\p{C}\\p{Z}]");
+ private static final Pattern SPECIAL_CHAR_PATTERN
+ = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]");
+ private static final Consumer<MessagingMessage> REMOVE_MESSAGE
+ = MessagingMessage::removeMessage;
+ public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+ public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+ public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+ public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR
+ = new MessagingPropertyAnimator();
+ private List<MessagingMessage> mMessages = new ArrayList<>();
+ private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
+ private MessagingLinearLayout mMessagingLinearLayout;
+ private boolean mShowHistoricMessages;
+ private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
+ private TextView mTitleView;
+ private int mLayoutColor;
+ private int mSenderTextColor;
+ private int mMessageTextColor;
+ private int mAvatarSize;
+ private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private Paint mTextPaint = new Paint();
+ private CharSequence mConversationTitle;
+ private Icon mAvatarReplacement;
+ private boolean mIsOneToOne;
+ private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
+ private Person mUser;
+ private CharSequence mNameReplacement;
+ private boolean mDisplayImagesAtEnd;
+ private ImageResolver mImageResolver;
+
+ public MessagingLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mMessagingLinearLayout = findViewById(R.id.notification_messaging);
+ mMessagingLinearLayout.setMessagingLayout(this);
+ // We still want to clip, but only on the top, since views can temporarily out of bounds
+ // during transitions.
+ DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);
+ Rect rect = new Rect(0, 0, size, size);
+ mMessagingLinearLayout.setClipBounds(rect);
+ mTitleView = findViewById(R.id.title);
+ mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+ mTextPaint.setAntiAlias(true);
+ }
+
+ @RemotableViewMethod
+ public void setAvatarReplacement(Icon icon) {
+ mAvatarReplacement = icon;
+ }
+
+ @RemotableViewMethod
+ public void setNameReplacement(CharSequence nameReplacement) {
+ mNameReplacement = nameReplacement;
+ }
+
+ /**
+ * Set this layout to show the collapsed representation.
+ *
+ * @param isCollapsed is it collapsed
+ */
+ @RemotableViewMethod
+ public void setIsCollapsed(boolean isCollapsed) {
+ mDisplayImagesAtEnd = isCollapsed;
+ }
+
+ @RemotableViewMethod
+ public void setLargeIcon(Icon largeIcon) {
+ // Unused
+ }
+
+ /**
+ * Sets the conversation title of this conversation.
+ *
+ * @param conversationTitle the conversation title
+ */
+ @RemotableViewMethod
+ public void setConversationTitle(CharSequence conversationTitle) {
+ // Unused
+ }
+
+ @RemotableViewMethod
+ public void setData(Bundle extras) {
+ Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
+ List<Notification.MessagingStyle.Message> newMessages
+ = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
+ Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
+ List<Notification.MessagingStyle.Message> newHistoricMessages
+ = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+ setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
+ mConversationTitle = null;
+ TextView headerText = findViewById(R.id.header_text);
+ if (headerText != null) {
+ mConversationTitle = headerText.getText();
+ }
+ RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
+ extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ addRemoteInputHistoryToMessages(newMessages, history);
+ boolean showSpinner =
+ extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
+ bind(newMessages, newHistoricMessages, showSpinner);
+ }
+
+ @Override
+ public void setImageResolver(ImageResolver resolver) {
+ mImageResolver = resolver;
+ }
+
+ private void addRemoteInputHistoryToMessages(
+ List<Notification.MessagingStyle.Message> newMessages,
+ RemoteInputHistoryItem[] remoteInputHistory) {
+ if (remoteInputHistory == null || remoteInputHistory.length == 0) {
+ return;
+ }
+ for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
+ RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
+ Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
+ historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
+ if (historyMessage.getUri() != null) {
+ message.setData(historyMessage.getMimeType(), historyMessage.getUri());
+ }
+ newMessages.add(message);
+ }
+ }
+
+ private void bind(List<Notification.MessagingStyle.Message> newMessages,
+ List<Notification.MessagingStyle.Message> newHistoricMessages,
+ boolean showSpinner) {
+
+ List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
+ true /* isHistoric */);
+ List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
+
+ ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
+ addMessagesToGroups(historicMessages, messages, showSpinner);
+
+ // Let's first check which groups were removed altogether and remove them in one animation
+ removeGroups(oldGroups);
+
+ // Let's remove the remaining messages
+ mMessages.forEach(REMOVE_MESSAGE);
+ mHistoricMessages.forEach(REMOVE_MESSAGE);
+
+ mMessages = messages;
+ mHistoricMessages = historicMessages;
+
+ updateHistoricMessageVisibility();
+ updateTitleAndNamesDisplay();
+ }
+
+ private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
+ int size = oldGroups.size();
+ for (int i = 0; i < size; i++) {
+ MessagingGroup group = oldGroups.get(i);
+ if (!mGroups.contains(group)) {
+ List<MessagingMessage> messages = group.getMessages();
+ Runnable endRunnable = () -> {
+ mMessagingLinearLayout.removeTransientView(group);
+ group.recycle();
+ };
+
+ boolean wasShown = group.isShown();
+ mMessagingLinearLayout.removeView(group);
+ if (wasShown && !MessagingLinearLayout.isGone(group)) {
+ mMessagingLinearLayout.addTransientView(group, 0);
+ group.removeGroupAnimated(endRunnable);
+ } else {
+ endRunnable.run();
+ }
+ mMessages.removeAll(messages);
+ mHistoricMessages.removeAll(messages);
+ }
+ }
+ }
+
+ private void updateTitleAndNamesDisplay() {
+ ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
+ ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
+ for (int i = 0; i < mGroups.size(); i++) {
+ MessagingGroup group = mGroups.get(i);
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
+ continue;
+ }
+ if (!uniqueNames.containsKey(senderName)) {
+ // Only use visible characters to get uniqueNames
+ String pureSenderName = IGNORABLE_CHAR_PATTERN
+ .matcher(senderName).replaceAll("" /* replacement */);
+ char c = pureSenderName.charAt(0);
+ if (uniqueCharacters.containsKey(c)) {
+ // this character was already used, lets make it more unique. We first need to
+ // resolve the existing character if it exists
+ CharSequence existingName = uniqueCharacters.get(c);
+ if (existingName != null) {
+ uniqueNames.put(existingName, findNameSplit((String) existingName));
+ uniqueCharacters.put(c, null);
+ }
+ uniqueNames.put(senderName, findNameSplit((String) senderName));
+ } else {
+ uniqueNames.put(senderName, Character.toString(c));
+ uniqueCharacters.put(c, pureSenderName);
+ }
+ }
+ }
+
+ // Now that we have the correct symbols, let's look what we have cached
+ ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
+ for (int i = 0; i < mGroups.size(); i++) {
+ // Let's now set the avatars
+ MessagingGroup group = mGroups.get(i);
+ boolean isOwnMessage = group.getSender() == mUser;
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
+ || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
+ continue;
+ }
+ String symbol = uniqueNames.get(senderName);
+ Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
+ symbol, mLayoutColor);
+ if (cachedIcon != null) {
+ cachedAvatars.put(senderName, cachedIcon);
+ }
+ }
+
+ for (int i = 0; i < mGroups.size(); i++) {
+ // Let's now set the avatars
+ MessagingGroup group = mGroups.get(i);
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
+ continue;
+ }
+ if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
+ group.setAvatar(mAvatarReplacement);
+ } else {
+ Icon cachedIcon = cachedAvatars.get(senderName);
+ if (cachedIcon == null) {
+ cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
+ mLayoutColor);
+ cachedAvatars.put(senderName, cachedIcon);
+ }
+ group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
+ mLayoutColor);
+ }
+ }
+ }
+
+ public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
+ if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) ||
+ SPECIAL_CHAR_PATTERN.matcher(symbol).find()) {
+ Icon avatarIcon = Icon.createWithResource(getContext(),
+ com.android.internal.R.drawable.messaging_user);
+ avatarIcon.setTint(findColor(senderName, layoutColor));
+ return avatarIcon;
+ } else {
+ Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ float radius = mAvatarSize / 2.0f;
+ int color = findColor(senderName, layoutColor);
+ mPaint.setColor(color);
+ canvas.drawCircle(radius, radius, radius, mPaint);
+ boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
+ mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
+ mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
+ int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
+ canvas.drawText(symbol, radius, yPos, mTextPaint);
+ return Icon.createWithBitmap(bitmap);
+ }
+ }
+
+ private int findColor(CharSequence senderName, int layoutColor) {
+ double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
+ float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
+
+ // we need to offset the range if the luminance is too close to the borders
+ shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
+ shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
+ return ContrastColorUtil.getShiftedColor(layoutColor,
+ (int) (shift * COLOR_SHIFT_AMOUNT));
+ }
+
+ private String findNameSplit(String existingName) {
+ String[] split = existingName.split(" ");
+ if (split.length > 1) {
+ return Character.toString(split[0].charAt(0))
+ + Character.toString(split[1].charAt(0));
+ }
+ return existingName.substring(0, 1);
+ }
+
+ @RemotableViewMethod
+ public void setLayoutColor(int color) {
+ mLayoutColor = color;
+ }
+
+ @RemotableViewMethod
+ public void setIsOneToOne(boolean oneToOne) {
+ mIsOneToOne = oneToOne;
+ }
+
+ @RemotableViewMethod
+ public void setSenderTextColor(int color) {
+ mSenderTextColor = color;
+ }
+
+
+ /**
+ * @param color the color of the notification background
+ */
+ @RemotableViewMethod
+ public void setNotificationBackgroundColor(int color) {
+ // Nothing to do with this
+ }
+
+ @RemotableViewMethod
+ public void setMessageTextColor(int color) {
+ mMessageTextColor = color;
+ }
+
+ public void setUser(Person user) {
+ mUser = user;
+ if (mUser.getIcon() == null) {
+ Icon userIcon = Icon.createWithResource(getContext(),
+ com.android.internal.R.drawable.messaging_user);
+ userIcon.setTint(mLayoutColor);
+ mUser = mUser.toBuilder().setIcon(userIcon).build();
+ }
+ }
+
+ private void addMessagesToGroups(List<MessagingMessage> historicMessages,
+ List<MessagingMessage> messages, boolean showSpinner) {
+ // Let's first find our groups!
+ List<List<MessagingMessage>> groups = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
+
+ // Lets first find the groups
+ findGroups(historicMessages, messages, groups, senders);
+
+ // Let's now create the views and reorder them accordingly
+ createGroupViews(groups, senders, showSpinner);
+ }
+
+ private void createGroupViews(List<List<MessagingMessage>> groups,
+ List<Person> senders, boolean showSpinner) {
+ mGroups.clear();
+ for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
+ List<MessagingMessage> group = groups.get(groupIndex);
+ MessagingGroup newGroup = null;
+ // we'll just take the first group that exists or create one there is none
+ for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
+ MessagingMessage message = group.get(messageIndex);
+ newGroup = message.getGroup();
+ if (newGroup != null) {
+ break;
+ }
+ }
+ if (newGroup == null) {
+ newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
+ mAddedGroups.add(newGroup);
+ }
+ newGroup.setImageDisplayLocation(mDisplayImagesAtEnd
+ ? IMAGE_DISPLAY_LOCATION_AT_END
+ : IMAGE_DISPLAY_LOCATION_INLINE);
+ newGroup.setIsInConversation(false);
+ newGroup.setLayoutColor(mLayoutColor);
+ newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
+ Person sender = senders.get(groupIndex);
+ CharSequence nameOverride = null;
+ if (sender != mUser && mNameReplacement != null) {
+ nameOverride = mNameReplacement;
+ }
+ newGroup.setSender(sender, nameOverride);
+ newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
+ mGroups.add(newGroup);
+
+ if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
+ mMessagingLinearLayout.removeView(newGroup);
+ mMessagingLinearLayout.addView(newGroup, groupIndex);
+ }
+ newGroup.setMessages(group);
+ }
+ }
+
+ private void findGroups(List<MessagingMessage> historicMessages,
+ List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
+ List<Person> senders) {
+ CharSequence currentSenderKey = null;
+ List<MessagingMessage> currentGroup = null;
+ int histSize = historicMessages.size();
+ for (int i = 0; i < histSize + messages.size(); i++) {
+ MessagingMessage message;
+ if (i < histSize) {
+ message = historicMessages.get(i);
+ } else {
+ message = messages.get(i - histSize);
+ }
+ boolean isNewGroup = currentGroup == null;
+ Person sender = message.getMessage().getSenderPerson();
+ CharSequence key = sender == null ? null
+ : sender.getKey() == null ? sender.getName() : sender.getKey();
+ isNewGroup |= !TextUtils.equals(key, currentSenderKey);
+ if (isNewGroup) {
+ currentGroup = new ArrayList<>();
+ groups.add(currentGroup);
+ if (sender == null) {
+ sender = mUser;
+ }
+ senders.add(sender);
+ currentSenderKey = key;
+ }
+ currentGroup.add(message);
+ }
+ }
+
+ /**
+ * Creates new messages, reusing existing ones if they are available.
+ *
+ * @param newMessages the messages to parse.
+ */
+ private List<MessagingMessage> createMessages(
+ List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
+ List<MessagingMessage> result = new ArrayList<>();
+ for (int i = 0; i < newMessages.size(); i++) {
+ Notification.MessagingStyle.Message m = newMessages.get(i);
+ MessagingMessage message = findAndRemoveMatchingMessage(m);
+ if (message == null) {
+ message = MessagingMessage.createMessage(this, m, mImageResolver);
+ }
+ message.setIsHistoric(historic);
+ result.add(message);
+ }
+ return result;
+ }
+
+ private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
+ for (int i = 0; i < mMessages.size(); i++) {
+ MessagingMessage existing = mMessages.get(i);
+ if (existing.sameAs(m)) {
+ mMessages.remove(i);
+ return existing;
+ }
+ }
+ for (int i = 0; i < mHistoricMessages.size(); i++) {
+ MessagingMessage existing = mHistoricMessages.get(i);
+ if (existing.sameAs(m)) {
+ mHistoricMessages.remove(i);
+ return existing;
+ }
+ }
+ return null;
+ }
+
+ public void showHistoricMessages(boolean show) {
+ mShowHistoricMessages = show;
+ updateHistoricMessageVisibility();
+ }
+
+ private void updateHistoricMessageVisibility() {
+ int numHistoric = mHistoricMessages.size();
+ for (int i = 0; i < numHistoric; i++) {
+ MessagingMessage existing = mHistoricMessages.get(i);
+ existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
+ }
+ int numGroups = mGroups.size();
+ for (int i = 0; i < numGroups; i++) {
+ MessagingGroup group = mGroups.get(i);
+ int visibleChildren = 0;
+ List<MessagingMessage> messages = group.getMessages();
+ int numGroupMessages = messages.size();
+ for (int j = 0; j < numGroupMessages; j++) {
+ MessagingMessage message = messages.get(j);
+ if (message.getVisibility() != GONE) {
+ visibleChildren++;
+ }
+ }
+ if (visibleChildren > 0 && group.getVisibility() == GONE) {
+ group.setVisibility(VISIBLE);
+ } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
+ group.setVisibility(GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (!mAddedGroups.isEmpty()) {
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ for (MessagingGroup group : mAddedGroups) {
+ if (!group.isShown()) {
+ continue;
+ }
+ MessagingPropertyAnimator.fadeIn(group.getAvatar());
+ MessagingPropertyAnimator.fadeIn(group.getSenderView());
+ MessagingPropertyAnimator.startLocalTranslationFrom(group,
+ group.getHeight(), LINEAR_OUT_SLOW_IN);
+ }
+ mAddedGroups.clear();
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ });
+ }
+ }
+
+ public MessagingLinearLayout getMessagingLinearLayout() {
+ return mMessagingLinearLayout;
+ }
+
+ public ArrayList<MessagingGroup> getMessagingGroups() {
+ return mGroups;
+ }
+
+ @Override
+ public void setMessagingClippingDisabled(boolean clippingDisabled) {
+ // Don't do anything, this is only used for the ConversationLayout
+ }
+}
diff --git a/com/android/internal/widget/MessagingLinearLayout.java b/com/android/internal/widget/MessagingLinearLayout.java
new file mode 100644
index 0000000..ac04862
--- /dev/null
+++ b/com/android/internal/widget/MessagingLinearLayout.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+/**
+ * A custom-built layout for the Notification.MessagingStyle.
+ *
+ * Evicts children until they all fit.
+ */
[email protected]
+public class MessagingLinearLayout extends ViewGroup {
+
+ /**
+ * Spacing to be applied between views.
+ */
+ private int mSpacing;
+
+ private int mMaxDisplayedLines = Integer.MAX_VALUE;
+
+ private IMessagingLayout mMessagingLayout;
+
+ public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.MessagingLinearLayout, 0,
+ 0);
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case R.styleable.MessagingLinearLayout_spacing:
+ mSpacing = a.getDimensionPixelSize(i, 0);
+ break;
+ }
+ }
+
+ a.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // This is essentially a bottom-up linear layout that only adds children that fit entirely
+ // up to a maximum height.
+ int targetHeight = MeasureSpec.getSize(heightMeasureSpec);
+ switch (MeasureSpec.getMode(heightMeasureSpec)) {
+ case MeasureSpec.UNSPECIFIED:
+ targetHeight = Integer.MAX_VALUE;
+ break;
+ }
+
+ // Now that we know which views to take, fix up the indents and see what width we get.
+ int measuredWidth = mPaddingLeft + mPaddingRight;
+ final int count = getChildCount();
+ int totalHeight;
+ for (int i = 0; i < count; ++i) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.hide = true;
+ if (child instanceof MessagingChild) {
+ MessagingChild messagingChild = (MessagingChild) child;
+ // Whenever we encounter the message first, it's always first in the layout
+ messagingChild.setIsFirstInLayout(true);
+ }
+ }
+
+ totalHeight = mPaddingTop + mPaddingBottom;
+ boolean first = true;
+ int linesRemaining = mMaxDisplayedLines;
+ // Starting from the bottom: we measure every view as if it were the only one. If it still
+ // fits, we take it, otherwise we stop there.
+ MessagingChild previousChild = null;
+ View previousView = null;
+ int previousChildHeight = 0;
+ int previousTotalHeight = 0;
+ int previousLinesConsumed = 0;
+ for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) {
+ if (getChildAt(i).getVisibility() == GONE) {
+ continue;
+ }
+ final View child = getChildAt(i);
+ LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+ MessagingChild messagingChild = null;
+ int spacing = mSpacing;
+ int previousChildIncrease = 0;
+ if (child instanceof MessagingChild) {
+ // We need to remeasure the previous child again if it's not the first anymore
+ if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) {
+ previousChild.setIsFirstInLayout(false);
+ measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec,
+ previousTotalHeight - previousChildHeight);
+ previousChildIncrease = previousView.getMeasuredHeight() - previousChildHeight;
+ linesRemaining -= previousChild.getConsumedLines() - previousLinesConsumed;
+ }
+ messagingChild = (MessagingChild) child;
+ messagingChild.setMaxDisplayedLines(linesRemaining);
+ spacing += messagingChild.getExtraSpacing();
+ }
+ spacing = first ? 0 : spacing;
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight
+ - mPaddingTop - mPaddingBottom + spacing);
+
+ final int childHeight = child.getMeasuredHeight();
+ int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin +
+ lp.bottomMargin + spacing + previousChildIncrease);
+ int measureType = MessagingChild.MEASURED_NORMAL;
+ if (messagingChild != null) {
+ measureType = messagingChild.getMeasuredType();
+ }
+
+ // We never measure the first item as too small, we want to at least show something.
+ boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL && !first;
+ boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED
+ || measureType == MessagingChild.MEASURED_TOO_SMALL && first;
+ boolean showView = newHeight <= targetHeight && !isTooSmall;
+ if (showView) {
+ if (messagingChild != null) {
+ previousLinesConsumed = messagingChild.getConsumedLines();
+ linesRemaining -= previousLinesConsumed;
+ previousChild = messagingChild;
+ previousView = child;
+ previousChildHeight = childHeight;
+ previousTotalHeight = totalHeight;
+ }
+ totalHeight = newHeight;
+ measuredWidth = Math.max(measuredWidth,
+ child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
+ + mPaddingLeft + mPaddingRight);
+ lp.hide = false;
+ if (isShortened || linesRemaining <= 0) {
+ break;
+ }
+ } else {
+ // We now became too short, let's make sure to reset any previous views to be first
+ // and remeasure it.
+ if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) {
+ previousChild.setIsFirstInLayout(true);
+ // We need to remeasure the previous child again since it became first
+ measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec,
+ previousTotalHeight - previousChildHeight);
+ // The totalHeight is already correct here since we only set it during the
+ // first pass
+ }
+ break;
+ }
+ first = false;
+ }
+
+ setMeasuredDimension(
+ resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
+ widthMeasureSpec),
+ Math.max(getSuggestedMinimumHeight(), totalHeight));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int paddingLeft = mPaddingLeft;
+
+ int childTop;
+
+ // Where right end of child should go
+ final int width = right - left;
+ final int childRight = width - mPaddingRight;
+
+ final int layoutDirection = getLayoutDirection();
+ final int count = getChildCount();
+
+ childTop = mPaddingTop;
+
+ boolean first = true;
+ final boolean shown = isShown();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ MessagingChild messagingChild = (MessagingChild) child;
+
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ int childLeft;
+ if (layoutDirection == LAYOUT_DIRECTION_RTL) {
+ childLeft = childRight - childWidth - lp.rightMargin;
+ } else {
+ childLeft = paddingLeft + lp.leftMargin;
+ }
+ if (lp.hide) {
+ if (shown && lp.visibleBefore) {
+ // We still want to lay out the child to have great animations
+ child.layout(childLeft, childTop, childLeft + childWidth,
+ childTop + lp.lastVisibleHeight);
+ messagingChild.hideAnimated();
+ }
+ lp.visibleBefore = false;
+ continue;
+ } else {
+ lp.visibleBefore = true;
+ lp.lastVisibleHeight = childHeight;
+ }
+
+ if (!first) {
+ childTop += mSpacing;
+ }
+
+ childTop += lp.topMargin;
+ child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+
+ childTop += childHeight + lp.bottomMargin;
+
+ first = false;
+ }
+ }
+
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.hide) {
+ MessagingChild messagingChild = (MessagingChild) child;
+ if (!messagingChild.isHidingAnimated()) {
+ return true;
+ }
+ }
+ return super.drawChild(canvas, child, drawingTime);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(mContext, attrs);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ LayoutParams copy = new LayoutParams(lp.width, lp.height);
+ if (lp instanceof MarginLayoutParams) {
+ copy.copyMarginsFrom((MarginLayoutParams) lp);
+ }
+ return copy;
+ }
+
+ public static boolean isGone(View view) {
+ if (view.getVisibility() == View.GONE) {
+ return true;
+ }
+ final ViewGroup.LayoutParams lp = view.getLayoutParams();
+ if (lp instanceof MessagingLinearLayout.LayoutParams
+ && ((MessagingLinearLayout.LayoutParams) lp).hide) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sets how many lines should be displayed at most
+ */
+ @RemotableViewMethod
+ public void setMaxDisplayedLines(int numberLines) {
+ mMaxDisplayedLines = numberLines;
+ }
+
+ public void setMessagingLayout(IMessagingLayout layout) {
+ mMessagingLayout = layout;
+ }
+
+ public IMessagingLayout getMessagingLayout() {
+ return mMessagingLayout;
+ }
+
+ public interface MessagingChild {
+ int MEASURED_NORMAL = 0;
+ int MEASURED_SHORTENED = 1;
+ int MEASURED_TOO_SMALL = 2;
+
+ int getMeasuredType();
+ int getConsumedLines();
+ void setMaxDisplayedLines(int lines);
+ void hideAnimated();
+ boolean isHidingAnimated();
+
+ /**
+ * Set that this view is first in layout. Relevant and only set if
+ * {@link #hasDifferentHeightWhenFirst()}.
+ * @param first is this first?
+ */
+ default void setIsFirstInLayout(boolean first) {}
+
+ /**
+ * @return if this layout has different height it is first in the layout
+ */
+ default boolean hasDifferentHeightWhenFirst() {
+ return false;
+ }
+ default int getExtraSpacing() {
+ return 0;
+ }
+ }
+
+ public static class LayoutParams extends MarginLayoutParams {
+
+ public boolean hide = false;
+ public boolean visibleBefore = false;
+ public int lastVisibleHeight;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+ }
+}
diff --git a/com/android/internal/widget/MessagingMessage.java b/com/android/internal/widget/MessagingMessage.java
new file mode 100644
index 0000000..8c84379
--- /dev/null
+++ b/com/android/internal/widget/MessagingMessage.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.view.View;
+
+import java.util.Objects;
+
+/**
+ * A message of a {@link MessagingLayout}.
+ */
+public interface MessagingMessage extends MessagingLinearLayout.MessagingChild {
+
+ /**
+ * Prefix for supported image MIME types
+ **/
+ String IMAGE_MIME_TYPE_PREFIX = "image/";
+
+ static MessagingMessage createMessage(IMessagingLayout layout,
+ Notification.MessagingStyle.Message m, ImageResolver resolver) {
+ if (hasImage(m) && !ActivityManager.isLowRamDeviceStatic()) {
+ return MessagingImageMessage.createMessage(layout, m, resolver);
+ } else {
+ return MessagingTextMessage.createMessage(layout, m);
+ }
+ }
+
+ static void dropCache() {
+ MessagingTextMessage.dropCache();
+ MessagingImageMessage.dropCache();
+ }
+
+ static boolean hasImage(Notification.MessagingStyle.Message m) {
+ return m.getDataUri() != null
+ && m.getDataMimeType() != null
+ && m.getDataMimeType().startsWith(IMAGE_MIME_TYPE_PREFIX);
+ }
+
+ /**
+ * Set a message for this view.
+ * @return true if setting the message worked
+ */
+ default boolean setMessage(Notification.MessagingStyle.Message message) {
+ getState().setMessage(message);
+ return true;
+ }
+
+ default Notification.MessagingStyle.Message getMessage() {
+ return getState().getMessage();
+ }
+
+ default boolean sameAs(Notification.MessagingStyle.Message message) {
+ Notification.MessagingStyle.Message ownMessage = getMessage();
+ if (!Objects.equals(message.getText(), ownMessage.getText())) {
+ return false;
+ }
+ if (!Objects.equals(message.getSender(), ownMessage.getSender())) {
+ return false;
+ }
+ boolean hasRemoteInputHistoryChanged = message.isRemoteInputHistory()
+ != ownMessage.isRemoteInputHistory();
+ // When the remote input history has changed, we want to regard messages equal even when
+ // the timestamp changes. The main reason is that the message that the system inserts
+ // will have a different time set than the one that the app will update us with and we
+ // still want to reuse that message.
+ if (!hasRemoteInputHistoryChanged
+ && !Objects.equals(message.getTimestamp(), ownMessage.getTimestamp())) {
+ return false;
+ }
+ if (!Objects.equals(message.getDataMimeType(), ownMessage.getDataMimeType())) {
+ return false;
+ }
+ if (!Objects.equals(message.getDataUri(), ownMessage.getDataUri())) {
+ return false;
+ }
+ return true;
+ }
+
+ default boolean sameAs(MessagingMessage message) {
+ return sameAs(message.getMessage());
+ }
+
+ default void removeMessage() {
+ getGroup().removeMessage(this);
+ }
+
+ default void setMessagingGroup(MessagingGroup group) {
+ getState().setGroup(group);
+ }
+
+ default void setIsHistoric(boolean isHistoric) {
+ getState().setIsHistoric(isHistoric);
+ }
+
+ default MessagingGroup getGroup() {
+ return getState().getGroup();
+ }
+
+ default void setIsHidingAnimated(boolean isHiding) {
+ getState().setIsHidingAnimated(isHiding);
+ }
+
+ @Override
+ default boolean isHidingAnimated() {
+ return getState().isHidingAnimated();
+ }
+
+ @Override
+ default void hideAnimated() {
+ setIsHidingAnimated(true);
+ getGroup().performRemoveAnimation(getView(), () -> setIsHidingAnimated(false));
+ }
+
+ default boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ default void recycle() {
+ getState().recycle();
+ }
+
+ default View getView() {
+ return (View) this;
+ }
+
+ default void setColor(int textColor) {}
+
+ MessagingMessageState getState();
+
+ void setVisibility(int visibility);
+
+ int getVisibility();
+}
diff --git a/com/android/internal/widget/MessagingMessageState.java b/com/android/internal/widget/MessagingMessageState.java
new file mode 100644
index 0000000..1ba2b51
--- /dev/null
+++ b/com/android/internal/widget/MessagingMessageState.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+import android.app.Notification;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+/**
+ * Shared state and implementation for MessagingMessages. Used to share common implementations.
+ */
+public class MessagingMessageState {
+ private final View mHostView;
+ private Notification.MessagingStyle.Message mMessage;
+ private MessagingGroup mGroup;
+ private boolean mIsHistoric;
+ private boolean mIsHidingAnimated;
+
+ MessagingMessageState(View hostView) {
+ mHostView = hostView;
+ }
+
+ public void setMessage(Notification.MessagingStyle.Message message) {
+ mMessage = message;
+ }
+
+ public Notification.MessagingStyle.Message getMessage() {
+ return mMessage;
+ }
+
+ public void setGroup(MessagingGroup group) {
+ mGroup = group;
+ }
+
+ public MessagingGroup getGroup() {
+ return mGroup;
+ }
+
+ public void setIsHistoric(boolean isHistoric) {
+ mIsHistoric = isHistoric;
+ }
+
+ public void setIsHidingAnimated(boolean isHiding) {
+ ViewParent parent = mHostView.getParent();
+ mIsHidingAnimated = isHiding;
+ mHostView.invalidate();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).invalidate();
+ }
+ }
+
+ public boolean isHidingAnimated() {
+ return mIsHidingAnimated;
+ }
+
+ public View getHostView() {
+ return mHostView;
+ }
+
+ public void recycle() {
+ mHostView.setAlpha(1.0f);
+ mHostView.setTranslationY(0);
+ MessagingPropertyAnimator.recycle(mHostView);
+ mIsHidingAnimated = false;
+ mIsHistoric = false;
+ mGroup = null;
+ mMessage = null;
+ }
+}
diff --git a/com/android/internal/widget/MessagingPropertyAnimator.java b/com/android/internal/widget/MessagingPropertyAnimator.java
new file mode 100644
index 0000000..a3a75c0
--- /dev/null
+++ b/com/android/internal/widget/MessagingPropertyAnimator.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.util.IntProperty;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import com.android.internal.R;
+
+/**
+ * A listener that automatically starts animations when the layout bounds change.
+ */
+public class MessagingPropertyAnimator implements View.OnLayoutChangeListener {
+ private static final long APPEAR_ANIMATION_LENGTH = 210;
+ public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+ public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
+ private static final int TAG_TOP_ANIMATOR = R.id.tag_top_animator;
+ private static final int TAG_TOP = R.id.tag_top_override;
+ private static final int TAG_LAYOUT_TOP = R.id.tag_layout_top;
+ private static final int TAG_FIRST_LAYOUT = R.id.tag_is_first_layout;
+ private static final int TAG_ALPHA_ANIMATOR = R.id.tag_alpha_animator;
+ private static final ViewClippingUtil.ClippingParameters CLIPPING_PARAMETERS =
+ view -> view.getId() == com.android.internal.R.id.notification_messaging;
+ private static final IntProperty<View> TOP =
+ new IntProperty<View>("top") {
+ @Override
+ public void setValue(View object, int value) {
+ setTop(object, value);
+ }
+
+ @Override
+ public Integer get(View object) {
+ return getTop(object);
+ }
+ };
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ setLayoutTop(v, top);
+ if (isFirstLayout(v)) {
+ setFirstLayout(v, false /* first */);
+ setTop(v, top);
+ return;
+ }
+ startTopAnimation(v, getTop(v), top, MessagingLayout.FAST_OUT_SLOW_IN);
+ }
+
+ private static boolean isFirstLayout(View view) {
+ Boolean tag = (Boolean) view.getTag(TAG_FIRST_LAYOUT);
+ if (tag == null) {
+ return true;
+ }
+ return tag;
+ }
+
+ public static void recycle(View view) {
+ setFirstLayout(view, true /* first */);
+ }
+
+ private static void setFirstLayout(View view, boolean first) {
+ view.setTagInternal(TAG_FIRST_LAYOUT, first);
+ }
+
+ private static void setLayoutTop(View view, int top) {
+ view.setTagInternal(TAG_LAYOUT_TOP, top);
+ }
+
+ public static int getLayoutTop(View view) {
+ Integer tag = (Integer) view.getTag(TAG_LAYOUT_TOP);
+ if (tag == null) {
+ return getTop(view);
+ }
+ return tag;
+ }
+
+ /**
+ * Start a translation animation from a start offset to the laid out location
+ * @param view The view to animate
+ * @param startTranslation The starting translation to start from.
+ * @param interpolator The interpolator to use.
+ */
+ public static void startLocalTranslationFrom(View view, int startTranslation,
+ Interpolator interpolator) {
+ startTopAnimation(view, getTop(view) + startTranslation, getLayoutTop(view), interpolator);
+ }
+
+ /**
+ * Start a translation animation from a start offset to the laid out location
+ * @param view The view to animate
+ * @param endTranslation The end translation to go to.
+ * @param interpolator The interpolator to use.
+ */
+ public static void startLocalTranslationTo(View view, int endTranslation,
+ Interpolator interpolator) {
+ int top = getTop(view);
+ startTopAnimation(view, top, top + endTranslation, interpolator);
+ }
+
+ public static int getTop(View v) {
+ Integer tag = (Integer) v.getTag(TAG_TOP);
+ if (tag == null) {
+ return v.getTop();
+ }
+ return tag;
+ }
+
+ private static void setTop(View v, int value) {
+ v.setTagInternal(TAG_TOP, value);
+ updateTopAndBottom(v);
+ }
+
+ private static void updateTopAndBottom(View v) {
+ int top = getTop(v);
+ int height = v.getHeight();
+ v.setTop(top);
+ v.setBottom(height + top);
+ }
+
+ private static void startTopAnimation(final View v, int start, int end,
+ Interpolator interpolator) {
+ ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_TOP_ANIMATOR);
+ if (existing != null) {
+ existing.cancel();
+ }
+ if (!v.isShown() || start == end
+ || (MessagingLinearLayout.isGone(v) && !isHidingAnimated(v))) {
+ setTop(v, end);
+ return;
+ }
+ ObjectAnimator animator = ObjectAnimator.ofInt(v, TOP, start, end);
+ setTop(v, start);
+ animator.setInterpolator(interpolator);
+ animator.setDuration(APPEAR_ANIMATION_LENGTH);
+ animator.addListener(new AnimatorListenerAdapter() {
+ public boolean mCancelled;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ v.setTagInternal(TAG_TOP_ANIMATOR, null);
+ setClippingDeactivated(v, false);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+ });
+ setClippingDeactivated(v, true);
+ v.setTagInternal(TAG_TOP_ANIMATOR, animator);
+ animator.start();
+ }
+
+ private static boolean isHidingAnimated(View v) {
+ if (v instanceof MessagingLinearLayout.MessagingChild) {
+ return ((MessagingLinearLayout.MessagingChild) v).isHidingAnimated();
+ }
+ return false;
+ }
+
+ public static void fadeIn(final View v) {
+ ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_ALPHA_ANIMATOR);
+ if (existing != null) {
+ existing.cancel();
+ }
+ if (v.getVisibility() == View.INVISIBLE) {
+ v.setVisibility(View.VISIBLE);
+ }
+ ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.ALPHA,
+ 0.0f, 1.0f);
+ v.setAlpha(0.0f);
+ animator.setInterpolator(ALPHA_IN);
+ animator.setDuration(APPEAR_ANIMATION_LENGTH);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ v.setTagInternal(TAG_ALPHA_ANIMATOR, null);
+ updateLayerType(v, false /* animating */);
+ }
+ });
+ updateLayerType(v, true /* animating */);
+ v.setTagInternal(TAG_ALPHA_ANIMATOR, animator);
+ animator.start();
+ }
+
+ private static void updateLayerType(View view, boolean animating) {
+ if (view.hasOverlappingRendering() && animating) {
+ view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ } else if (view.getLayerType() == View.LAYER_TYPE_HARDWARE) {
+ view.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
+
+ public static void fadeOut(final View view, Runnable endAction) {
+ ObjectAnimator existing = (ObjectAnimator) view.getTag(TAG_ALPHA_ANIMATOR);
+ if (existing != null) {
+ existing.cancel();
+ }
+ if (!view.isShown() || (MessagingLinearLayout.isGone(view) && !isHidingAnimated(view))) {
+ view.setAlpha(0.0f);
+ if (endAction != null) {
+ endAction.run();
+ }
+ return;
+ }
+ ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA,
+ view.getAlpha(), 0.0f);
+ animator.setInterpolator(ALPHA_OUT);
+ animator.setDuration(APPEAR_ANIMATION_LENGTH);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setTagInternal(TAG_ALPHA_ANIMATOR, null);
+ updateLayerType(view, false /* animating */);
+ if (endAction != null) {
+ endAction.run();
+ }
+ }
+ });
+ updateLayerType(view, true /* animating */);
+ view.setTagInternal(TAG_ALPHA_ANIMATOR, animator);
+ animator.start();
+ }
+
+ public static void setClippingDeactivated(final View transformedView, boolean deactivated) {
+ ViewClippingUtil.setClippingDeactivated(transformedView, deactivated,
+ CLIPPING_PARAMETERS);
+ }
+
+ public static boolean isAnimatingTranslation(View v) {
+ return v.getTag(TAG_TOP_ANIMATOR) != null;
+ }
+
+ public static boolean isAnimatingAlpha(View v) {
+ return v.getTag(TAG_ALPHA_ANIMATOR) != null;
+ }
+
+ public static void setToLaidOutPosition(View view) {
+ setTop(view, getLayoutTop(view));
+ }
+}
diff --git a/com/android/internal/widget/MessagingTextMessage.java b/com/android/internal/widget/MessagingTextMessage.java
new file mode 100644
index 0000000..d778c59
--- /dev/null
+++ b/com/android/internal/widget/MessagingTextMessage.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.content.Context;
+import android.text.Layout;
+import android.util.AttributeSet;
+import android.util.Pools;
+import android.view.LayoutInflater;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+/**
+ * A message of a {@link MessagingLayout}.
+ */
[email protected]
+public class MessagingTextMessage extends ImageFloatingTextView implements MessagingMessage {
+
+ private static Pools.SimplePool<MessagingTextMessage> sInstancePool
+ = new Pools.SynchronizedPool<>(20);
+ private final MessagingMessageState mState = new MessagingMessageState(this);
+
+ public MessagingTextMessage(@NonNull Context context) {
+ super(context);
+ }
+
+ public MessagingTextMessage(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MessagingTextMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public MessagingTextMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public MessagingMessageState getState() {
+ return mState;
+ }
+
+ @Override
+ public boolean setMessage(Notification.MessagingStyle.Message message) {
+ MessagingMessage.super.setMessage(message);
+ setText(message.getText());
+ return true;
+ }
+
+ static MessagingMessage createMessage(IMessagingLayout layout,
+ Notification.MessagingStyle.Message m) {
+ MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout();
+ MessagingTextMessage createdMessage = sInstancePool.acquire();
+ if (createdMessage == null) {
+ createdMessage = (MessagingTextMessage) LayoutInflater.from(
+ layout.getContext()).inflate(
+ R.layout.notification_template_messaging_text_message,
+ messagingLinearLayout,
+ false);
+ createdMessage.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
+ }
+ createdMessage.setMessage(m);
+ return createdMessage;
+ }
+
+ public void recycle() {
+ MessagingMessage.super.recycle();
+ sInstancePool.release(this);
+ }
+
+ public static void dropCache() {
+ sInstancePool = new Pools.SynchronizedPool<>(10);
+ }
+
+ @Override
+ public int getMeasuredType() {
+ boolean measuredTooSmall = getMeasuredHeight()
+ < getLayoutHeight() + getPaddingTop() + getPaddingBottom();
+ if (measuredTooSmall && getLineCount() <= 1) {
+ return MEASURED_TOO_SMALL;
+ } else {
+ Layout layout = getLayout();
+ if (layout == null) {
+ return MEASURED_TOO_SMALL;
+ }
+ if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) {
+ return MEASURED_SHORTENED;
+ } else {
+ return MEASURED_NORMAL;
+ }
+ }
+ }
+
+ @Override
+ public void setMaxDisplayedLines(int lines) {
+ setMaxLines(lines);
+ }
+
+ @Override
+ public int getConsumedLines() {
+ return getLineCount();
+ }
+
+ public int getLayoutHeight() {
+ Layout layout = getLayout();
+ if (layout == null) {
+ return 0;
+ }
+ return layout.getHeight();
+ }
+
+ @Override
+ public void setColor(int color) {
+ setTextColor(color);
+ }
+}
diff --git a/com/android/internal/widget/NestedScrollingChild.java b/com/android/internal/widget/NestedScrollingChild.java
new file mode 100644
index 0000000..20285b5
--- /dev/null
+++ b/com/android/internal/widget/NestedScrollingChild.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+
+/**
+ * This interface should be implemented by {@link android.view.View View} subclasses that wish
+ * to support dispatching nested scrolling operations to a cooperating parent
+ * {@link android.view.ViewGroup ViewGroup}.
+ *
+ * <p>Classes implementing this interface should create a final instance of a
+ * {@link NestedScrollingChildHelper} as a field and delegate any View methods to the
+ * <code>NestedScrollingChildHelper</code> methods of the same signature.</p>
+ *
+ * <p>Views invoking nested scrolling functionality should always do so from the relevant
+ * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility
+ * shim static methods. This ensures interoperability with nested scrolling views on Android
+ * 5.0 Lollipop and newer.</p>
+ */
+public interface NestedScrollingChild {
+ /**
+ * Enable or disable nested scrolling for this view.
+ *
+ * <p>If this property is set to true the view will be permitted to initiate nested
+ * scrolling operations with a compatible parent view in the current hierarchy. If this
+ * view does not implement nested scrolling this will have no effect. Disabling nested scrolling
+ * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping}
+ * the nested scroll.</p>
+ *
+ * @param enabled true to enable nested scrolling, false to disable
+ *
+ * @see #isNestedScrollingEnabled()
+ */
+ void setNestedScrollingEnabled(boolean enabled);
+
+ /**
+ * Returns true if nested scrolling is enabled for this view.
+ *
+ * <p>If nested scrolling is enabled and this View class implementation supports it,
+ * this view will act as a nested scrolling child view when applicable, forwarding data
+ * about the scroll operation in progress to a compatible and cooperating nested scrolling
+ * parent.</p>
+ *
+ * @return true if nested scrolling is enabled
+ *
+ * @see #setNestedScrollingEnabled(boolean)
+ */
+ boolean isNestedScrollingEnabled();
+
+ /**
+ * Begin a nestable scroll operation along the given axes.
+ *
+ * <p>A view starting a nested scroll promises to abide by the following contract:</p>
+ *
+ * <p>The view will call startNestedScroll upon initiating a scroll operation. In the case
+ * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}.
+ * In the case of touch scrolling the nested scroll will be terminated automatically in
+ * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}.
+ * In the event of programmatic scrolling the caller must explicitly call
+ * {@link #stopNestedScroll()} to indicate the end of the nested scroll.</p>
+ *
+ * <p>If <code>startNestedScroll</code> returns true, a cooperative parent was found.
+ * If it returns false the caller may ignore the rest of this contract until the next scroll.
+ * Calling startNestedScroll while a nested scroll is already in progress will return true.</p>
+ *
+ * <p>At each incremental step of the scroll the caller should invoke
+ * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll}
+ * once it has calculated the requested scrolling delta. If it returns true the nested scrolling
+ * parent at least partially consumed the scroll and the caller should adjust the amount it
+ * scrolls by.</p>
+ *
+ * <p>After applying the remainder of the scroll delta the caller should invoke
+ * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing
+ * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat
+ * these values differently. See
+ * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}.
+ * </p>
+ *
+ * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}
+ * and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}.
+ * @return true if a cooperative parent was found and nested scrolling has been enabled for
+ * the current gesture.
+ *
+ * @see #stopNestedScroll()
+ * @see #dispatchNestedPreScroll(int, int, int[], int[])
+ * @see #dispatchNestedScroll(int, int, int, int, int[])
+ */
+ boolean startNestedScroll(int axes);
+
+ /**
+ * Stop a nested scroll in progress.
+ *
+ * <p>Calling this method when a nested scroll is not currently in progress is harmless.</p>
+ *
+ * @see #startNestedScroll(int)
+ */
+ void stopNestedScroll();
+
+ /**
+ * Returns true if this view has a nested scrolling parent.
+ *
+ * <p>The presence of a nested scrolling parent indicates that this view has initiated
+ * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.</p>
+ *
+ * @return whether this view has a nested scrolling parent
+ */
+ boolean hasNestedScrollingParent();
+
+ /**
+ * Dispatch one step of a nested scroll in progress.
+ *
+ * <p>Implementations of views that support nested scrolling should call this to report
+ * info about a scroll in progress to the current nested scrolling parent. If a nested scroll
+ * is not currently in progress or nested scrolling is not
+ * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.</p>
+ *
+ * <p>Compatible View implementations should also call
+ * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before
+ * consuming a component of the scroll event themselves.</p>
+ *
+ * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
+ * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step
+ * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view
+ * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view
+ * @param offsetInWindow Optional. If not null, on return this will contain the offset
+ * in local view coordinates of this view from before this operation
+ * to after it completes. View implementations may use this to adjust
+ * expected input coordinate tracking.
+ * @return true if the event was dispatched, false if it could not be dispatched.
+ * @see #dispatchNestedPreScroll(int, int, int[], int[])
+ */
+ boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
+
+ /**
+ * Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
+ *
+ * <p>Nested pre-scroll events are to nested scroll events what touch intercept is to touch.
+ * <code>dispatchNestedPreScroll</code> offers an opportunity for the parent view in a nested
+ * scrolling operation to consume some or all of the scroll operation before the child view
+ * consumes it.</p>
+ *
+ * @param dx Horizontal scroll distance in pixels
+ * @param dy Vertical scroll distance in pixels
+ * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx
+ * and consumed[1] the consumed dy.
+ * @param offsetInWindow Optional. If not null, on return this will contain the offset
+ * in local view coordinates of this view from before this operation
+ * to after it completes. View implementations may use this to adjust
+ * expected input coordinate tracking.
+ * @return true if the parent consumed some or all of the scroll delta
+ * @see #dispatchNestedScroll(int, int, int, int, int[])
+ */
+ boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
+
+ /**
+ * Dispatch a fling to a nested scrolling parent.
+ *
+ * <p>This method should be used to indicate that a nested scrolling child has detected
+ * suitable conditions for a fling. Generally this means that a touch scroll has ended with a
+ * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
+ * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
+ * along a scrollable axis.</p>
+ *
+ * <p>If a nested scrolling child view would normally fling but it is at the edge of
+ * its own content, it can use this method to delegate the fling to its nested scrolling
+ * parent instead. The parent may optionally consume the fling or observe a child fling.</p>
+ *
+ * @param velocityX Horizontal fling velocity in pixels per second
+ * @param velocityY Vertical fling velocity in pixels per second
+ * @param consumed true if the child consumed the fling, false otherwise
+ * @return true if the nested scrolling parent consumed or otherwise reacted to the fling
+ */
+ boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
+
+ /**
+ * Dispatch a fling to a nested scrolling parent before it is processed by this view.
+ *
+ * <p>Nested pre-fling events are to nested fling events what touch intercept is to touch
+ * and what nested pre-scroll is to nested scroll. <code>dispatchNestedPreFling</code>
+ * offsets an opportunity for the parent view in a nested fling to fully consume the fling
+ * before the child view consumes it. If this method returns <code>true</code>, a nested
+ * parent view consumed the fling and this view should not scroll as a result.</p>
+ *
+ * <p>For a better user experience, only one view in a nested scrolling chain should consume
+ * the fling at a time. If a parent view consumed the fling this method will return false.
+ * Custom view implementations should account for this in two ways:</p>
+ *
+ * <ul>
+ * <li>If a custom view is paged and needs to settle to a fixed page-point, do not
+ * call <code>dispatchNestedPreFling</code>; consume the fling and settle to a valid
+ * position regardless.</li>
+ * <li>If a nested parent does consume the fling, this view should not scroll at all,
+ * even to settle back to a valid idle position.</li>
+ * </ul>
+ *
+ * <p>Views should also not offer fling velocities to nested parent views along an axis
+ * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView}
+ * should not offer a horizontal fling velocity to its parents since scrolling along that
+ * axis is not permitted and carrying velocity along that motion does not make sense.</p>
+ *
+ * @param velocityX Horizontal fling velocity in pixels per second
+ * @param velocityY Vertical fling velocity in pixels per second
+ * @return true if a nested scrolling parent consumed the fling
+ */
+ boolean dispatchNestedPreFling(float velocityX, float velocityY);
+}
diff --git a/com/android/internal/widget/NotificationActionListLayout.java b/com/android/internal/widget/NotificationActionListLayout.java
new file mode 100644
index 0000000..c7ea781
--- /dev/null
+++ b/com/android/internal/widget/NotificationActionListLayout.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.RippleDrawable;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * Layout for notification actions that ensures that no action consumes more than their share of
+ * the remaining available width, and the last action consumes the remaining space.
+ */
[email protected]
+public class NotificationActionListLayout extends LinearLayout {
+
+ private final int mGravity;
+ private int mTotalWidth = 0;
+ private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>();
+ private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
+ private boolean mEmphasizedMode;
+ private int mDefaultPaddingBottom;
+ private int mDefaultPaddingTop;
+ private int mEmphasizedHeight;
+ private int mRegularHeight;
+
+ public NotificationActionListLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ int[] attrIds = { android.R.attr.gravity };
+ TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
+ mGravity = ta.getInt(0, 0);
+ ta.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mEmphasizedMode) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+ final int N = getChildCount();
+ int textViews = 0;
+ int otherViews = 0;
+ int notGoneChildren = 0;
+
+ for (int i = 0; i < N; i++) {
+ View c = getChildAt(i);
+ if (c instanceof TextView) {
+ textViews++;
+ } else {
+ otherViews++;
+ }
+ if (c.getVisibility() != GONE) {
+ notGoneChildren++;
+ }
+ }
+
+ // Rebuild the measure order if the number of children changed or the text length of
+ // any of the children changed.
+ boolean needRebuild = false;
+ if (textViews != mMeasureOrderTextViews.size()
+ || otherViews != mMeasureOrderOther.size()) {
+ needRebuild = true;
+ }
+ if (!needRebuild) {
+ final int size = mMeasureOrderTextViews.size();
+ for (int i = 0; i < size; i++) {
+ Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i);
+ if (pair.first != pair.second.getText().length()) {
+ needRebuild = true;
+ }
+ }
+ }
+
+ if (needRebuild) {
+ rebuildMeasureOrder(textViews, otherViews);
+ }
+
+ final boolean constrained =
+ MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
+
+ final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
+ final int otherSize = mMeasureOrderOther.size();
+ int usedWidth = 0;
+
+ int measuredChildren = 0;
+ for (int i = 0; i < N; i++) {
+ // Measure shortest children first. To avoid measuring twice, we approximate by looking
+ // at the text length.
+ View c;
+ if (i < otherSize) {
+ c = mMeasureOrderOther.get(i);
+ } else {
+ c = mMeasureOrderTextViews.get(i - otherSize).second;
+ }
+ if (c.getVisibility() == GONE) {
+ continue;
+ }
+ MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
+
+ int usedWidthForChild = usedWidth;
+ if (constrained) {
+ // Make sure that this child doesn't consume more than its share of the remaining
+ // total available space. Not used space will benefit subsequent views. Since we
+ // measure in the order of (approx.) size, a large view can still take more than its
+ // share if the others are small.
+ int availableWidth = innerWidth - usedWidth;
+ int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren);
+
+ usedWidthForChild = innerWidth - maxWidthForChild;
+ }
+
+ measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
+ heightMeasureSpec, 0 /* usedHeight */);
+
+ usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
+ measuredChildren++;
+ }
+
+ mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft;
+ setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+ resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
+ }
+
+ private void rebuildMeasureOrder(int capacityText, int capacityOther) {
+ clearMeasureOrder();
+ mMeasureOrderTextViews.ensureCapacity(capacityText);
+ mMeasureOrderOther.ensureCapacity(capacityOther);
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View c = getChildAt(i);
+ if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
+ mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(),
+ (TextView)c));
+ } else {
+ mMeasureOrderOther.add(c);
+ }
+ }
+ mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
+ }
+
+ private void clearMeasureOrder() {
+ mMeasureOrderOther.clear();
+ mMeasureOrderTextViews.clear();
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ clearMeasureOrder();
+ // For some reason ripples + notification actions seem to be an unhappy combination
+ // b/69474443 so just turn them off for now.
+ if (child.getBackground() instanceof RippleDrawable) {
+ ((RippleDrawable)child.getBackground()).setForceSoftware(true);
+ }
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ clearMeasureOrder();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (mEmphasizedMode) {
+ super.onLayout(changed, left, top, right, bottom);
+ return;
+ }
+ final boolean isLayoutRtl = isLayoutRtl();
+ final int paddingTop = mPaddingTop;
+ final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
+
+ int childTop;
+ int childLeft;
+ if (centerAligned) {
+ childLeft = mPaddingLeft + left + (right - left) / 2 - mTotalWidth / 2;
+ } else {
+ childLeft = mPaddingLeft;
+ int absoluteGravity = Gravity.getAbsoluteGravity(Gravity.START, getLayoutDirection());
+ if (absoluteGravity == Gravity.RIGHT) {
+ childLeft += right - left - mTotalWidth;
+ }
+ }
+
+
+ // Where bottom of child should go
+ final int height = bottom - top;
+
+ // Space available for child
+ int innerHeight = height - paddingTop - mPaddingBottom;
+
+ final int count = getChildCount();
+
+ int start = 0;
+ int dir = 1;
+ //In case of RTL, start drawing from the last child.
+ if (isLayoutRtl) {
+ start = count - 1;
+ dir = -1;
+ }
+
+ for (int i = 0; i < count; i++) {
+ final int childIndex = start + dir * i;
+ final View child = getChildAt(childIndex);
+ if (child.getVisibility() != GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ childTop = paddingTop + ((innerHeight - childHeight) / 2)
+ + lp.topMargin - lp.bottomMargin;
+
+ childLeft += lp.leftMargin;
+ child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+ childLeft += childWidth + lp.rightMargin;
+ }
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mDefaultPaddingBottom = getPaddingBottom();
+ mDefaultPaddingTop = getPaddingTop();
+ updateHeights();
+ }
+
+ private void updateHeights() {
+ int paddingTop = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin);
+ // same padding on bottom and at end
+ int paddingBottom = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin_end);
+ mEmphasizedHeight = paddingBottom + paddingTop + getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_action_emphasized_height);
+ mRegularHeight = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_action_list_height);
+ }
+
+ /**
+ * Set whether the list is in a mode where some actions are emphasized. This will trigger an
+ * equal measuring where all actions are full height and change a few parameters like
+ * the padding.
+ */
+ @RemotableViewMethod
+ public void setEmphasizedMode(boolean emphasizedMode) {
+ mEmphasizedMode = emphasizedMode;
+ int height;
+ if (emphasizedMode) {
+ int paddingTop = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin);
+ // same padding on bottom and at end
+ int paddingBottom = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin_end);
+ height = mEmphasizedHeight;
+ int buttonPaddingInternal = getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.button_inset_vertical_material);
+ setPaddingRelative(getPaddingStart(),
+ paddingTop - buttonPaddingInternal,
+ getPaddingEnd(),
+ paddingBottom - buttonPaddingInternal);
+ } else {
+ setPaddingRelative(getPaddingStart(),
+ mDefaultPaddingTop,
+ getPaddingEnd(),
+ mDefaultPaddingBottom);
+ height = mRegularHeight;
+ }
+ ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ layoutParams.height = height;
+ setLayoutParams(layoutParams);
+ }
+
+ public int getExtraMeasureHeight() {
+ if (mEmphasizedMode) {
+ return mEmphasizedHeight - mRegularHeight;
+ }
+ return 0;
+ }
+
+ public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
+ = (a, b) -> a.first.compareTo(b.first);
+}
diff --git a/com/android/internal/widget/NotificationExpandButton.java b/com/android/internal/widget/NotificationExpandButton.java
new file mode 100644
index 0000000..a499806
--- /dev/null
+++ b/com/android/internal/widget/NotificationExpandButton.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+/**
+ * An expand button in a notification
+ */
[email protected]
+public class NotificationExpandButton extends ImageView {
+
+ private int mOriginalNotificationColor;
+
+ public NotificationExpandButton(Context context) {
+ super(context);
+ }
+
+ public NotificationExpandButton(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NotificationExpandButton(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public NotificationExpandButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
+ super.getBoundsOnScreen(outRect, clipToParent);
+ extendRectToMinTouchSize(outRect);
+ }
+
+ @RemotableViewMethod
+ public void setOriginalNotificationColor(int color) {
+ mOriginalNotificationColor = color;
+ }
+
+ public int getOriginalNotificationColor() {
+ return mOriginalNotificationColor;
+ }
+
+ private void extendRectToMinTouchSize(Rect rect) {
+ int touchTargetSize = (int) (getResources().getDisplayMetrics().density * 48);
+ rect.left = rect.centerX() - touchTargetSize / 2;
+ rect.right = rect.left + touchTargetSize;
+ rect.top = rect.centerY() - touchTargetSize / 2;
+ rect.bottom = rect.top + touchTargetSize;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Button.class.getName());
+ }
+}
diff --git a/com/android/internal/widget/NumericTextView.java b/com/android/internal/widget/NumericTextView.java
new file mode 100644
index 0000000..c8f9011
--- /dev/null
+++ b/com/android/internal/widget/NumericTextView.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.StateSet;
+import android.view.KeyEvent;
+import android.widget.TextView;
+
+/**
+ * Extension of TextView that can handle displaying and inputting a range of
+ * numbers.
+ * <p>
+ * Clients of this view should never call {@link #setText(CharSequence)} or
+ * {@link #setHint(CharSequence)} directly. Instead, they should call
+ * {@link #setValue(int)} to modify the currently displayed value.
+ */
+public class NumericTextView extends TextView {
+ private static final int RADIX = 10;
+ private static final double LOG_RADIX = Math.log(RADIX);
+
+ private int mMinValue = 0;
+ private int mMaxValue = 99;
+
+ /** Number of digits in the maximum value. */
+ private int mMaxCount = 2;
+
+ private boolean mShowLeadingZeroes = true;
+
+ private int mValue;
+
+ /** Number of digits entered during editing mode. */
+ private int mCount;
+
+ /** Used to restore the value after an aborted edit. */
+ private int mPreviousValue;
+
+ private OnValueChangedListener mListener;
+
+ @UnsupportedAppUsage
+ public NumericTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Generate the hint text color based on disabled state.
+ final int textColorDisabled = getTextColors().getColorForState(StateSet.get(0), 0);
+ setHintTextColor(textColorDisabled);
+
+ setFocusable(true);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+
+ if (focused) {
+ mPreviousValue = mValue;
+ mValue = 0;
+ mCount = 0;
+
+ // Transfer current text to hint.
+ setHint(getText());
+ setText("");
+ } else {
+ if (mCount == 0) {
+ // No digits were entered, revert to previous value.
+ mValue = mPreviousValue;
+
+ setText(getHint());
+ setHint("");
+ }
+
+ // Ensure the committed value is within range.
+ if (mValue < mMinValue) {
+ mValue = mMinValue;
+ }
+
+ setValue(mValue);
+
+ if (mListener != null) {
+ mListener.onValueChanged(this, mValue, true, true);
+ }
+ }
+ }
+
+ /**
+ * Sets the currently displayed value.
+ * <p>
+ * The specified {@code value} must be within the range specified by
+ * {@link #setRange(int, int)} (e.g. between {@link #getRangeMinimum()}
+ * and {@link #getRangeMaximum()}).
+ *
+ * @param value the value to display
+ */
+ public final void setValue(int value) {
+ if (mValue != value) {
+ mValue = value;
+
+ updateDisplayedValue();
+ }
+ }
+
+ /**
+ * Returns the currently displayed value.
+ * <p>
+ * If the value is currently being edited, returns the live value which may
+ * not be within the range specified by {@link #setRange(int, int)}.
+ *
+ * @return the currently displayed value
+ */
+ public final int getValue() {
+ return mValue;
+ }
+
+ /**
+ * Sets the valid range (inclusive).
+ *
+ * @param minValue the minimum valid value (inclusive)
+ * @param maxValue the maximum valid value (inclusive)
+ */
+ public final void setRange(int minValue, int maxValue) {
+ if (mMinValue != minValue) {
+ mMinValue = minValue;
+ }
+
+ if (mMaxValue != maxValue) {
+ mMaxValue = maxValue;
+ mMaxCount = 1 + (int) (Math.log(maxValue) / LOG_RADIX);
+
+ updateMinimumWidth();
+ updateDisplayedValue();
+ }
+ }
+
+ /**
+ * @return the minimum value value (inclusive)
+ */
+ public final int getRangeMinimum() {
+ return mMinValue;
+ }
+
+ /**
+ * @return the maximum value value (inclusive)
+ */
+ public final int getRangeMaximum() {
+ return mMaxValue;
+ }
+
+ /**
+ * Sets whether this view shows leading zeroes.
+ * <p>
+ * When leading zeroes are shown, the displayed value will be padded
+ * with zeroes to the width of the maximum value as specified by
+ * {@link #setRange(int, int)} (see also {@link #getRangeMaximum()}.
+ * <p>
+ * For example, with leading zeroes shown, a maximum of 99 and value of
+ * 9 would display "09". A maximum of 100 and a value of 9 would display
+ * "009". With leading zeroes hidden, both cases would show "9".
+ *
+ * @param showLeadingZeroes {@code true} to show leading zeroes,
+ * {@code false} to hide them
+ */
+ public final void setShowLeadingZeroes(boolean showLeadingZeroes) {
+ if (mShowLeadingZeroes != showLeadingZeroes) {
+ mShowLeadingZeroes = showLeadingZeroes;
+
+ updateDisplayedValue();
+ }
+ }
+
+ public final boolean getShowLeadingZeroes() {
+ return mShowLeadingZeroes;
+ }
+
+ /**
+ * Computes the display value and updates the text of the view.
+ * <p>
+ * This method should be called whenever the current value or display
+ * properties (leading zeroes, max digits) change.
+ */
+ private void updateDisplayedValue() {
+ final String format;
+ if (mShowLeadingZeroes) {
+ format = "%0" + mMaxCount + "d";
+ } else {
+ format = "%d";
+ }
+
+ // Always use String.format() rather than Integer.toString()
+ // to obtain correctly localized values.
+ setText(String.format(format, mValue));
+ }
+
+ /**
+ * Computes the minimum width in pixels required to display all possible
+ * values and updates the minimum width of the view.
+ * <p>
+ * This method should be called whenever the maximum value changes.
+ */
+ private void updateMinimumWidth() {
+ final CharSequence previousText = getText();
+ int maxWidth = 0;
+
+ for (int i = 0; i < mMaxValue; i++) {
+ setText(String.format("%0" + mMaxCount + "d", i));
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ final int width = getMeasuredWidth();
+ if (width > maxWidth) {
+ maxWidth = width;
+ }
+ }
+
+ setText(previousText);
+ setMinWidth(maxWidth);
+ setMinimumWidth(maxWidth);
+ }
+
+ public final void setOnDigitEnteredListener(OnValueChangedListener listener) {
+ mListener = listener;
+ }
+
+ public final OnValueChangedListener getOnDigitEnteredListener() {
+ return mListener;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return isKeyCodeNumeric(keyCode)
+ || (keyCode == KeyEvent.KEYCODE_DEL)
+ || super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return isKeyCodeNumeric(keyCode)
+ || (keyCode == KeyEvent.KEYCODE_DEL)
+ || super.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return handleKeyUp(keyCode)
+ || super.onKeyUp(keyCode, event);
+ }
+
+ private boolean handleKeyUp(int keyCode) {
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ // Backspace removes the least-significant digit, if available.
+ if (mCount > 0) {
+ mValue /= RADIX;
+ mCount--;
+ }
+ } else if (isKeyCodeNumeric(keyCode)) {
+ if (mCount < mMaxCount) {
+ final int keyValue = numericKeyCodeToInt(keyCode);
+ final int newValue = mValue * RADIX + keyValue;
+ if (newValue <= mMaxValue) {
+ mValue = newValue;
+ mCount++;
+ }
+ }
+ } else {
+ return false;
+ }
+
+ final String formattedValue;
+ if (mCount > 0) {
+ // If the user types 01, we should always show the leading 0 even if
+ // getShowLeadingZeroes() is false. Preserve typed leading zeroes by
+ // using the number of digits entered as the format width.
+ formattedValue = String.format("%0" + mCount + "d", mValue);
+ } else {
+ formattedValue = "";
+ }
+
+ setText(formattedValue);
+
+ if (mListener != null) {
+ final boolean isValid = mValue >= mMinValue;
+ final boolean isFinished = mCount >= mMaxCount || mValue * RADIX > mMaxValue;
+ mListener.onValueChanged(this, mValue, isValid, isFinished);
+ }
+
+ return true;
+ }
+
+ private static boolean isKeyCodeNumeric(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1
+ || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3
+ || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5
+ || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
+ || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9;
+ }
+
+ private static int numericKeyCodeToInt(int keyCode) {
+ return keyCode - KeyEvent.KEYCODE_0;
+ }
+
+ public interface OnValueChangedListener {
+ /**
+ * Called when the value displayed by {@code view} changes.
+ *
+ * @param view the view whose value changed
+ * @param value the new value
+ * @param isValid {@code true} if the value is valid (e.g. within the
+ * range specified by {@link #setRange(int, int)}),
+ * {@code false} otherwise
+ * @param isFinished {@code true} if the no more digits may be entered,
+ * {@code false} if more digits may be entered
+ */
+ void onValueChanged(NumericTextView view, int value, boolean isValid, boolean isFinished);
+ }
+}
diff --git a/com/android/internal/widget/ObservableTextView.java b/com/android/internal/widget/ObservableTextView.java
new file mode 100644
index 0000000..1f3c296
--- /dev/null
+++ b/com/android/internal/widget/ObservableTextView.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import java.util.function.Consumer;
+
+/**
+ * A text view whose visibility can be observed.
+ */
[email protected]
+public class ObservableTextView extends TextView {
+
+ private Consumer<Integer> mOnVisibilityChangedListener;
+
+ public ObservableTextView(Context context) {
+ super(context);
+ }
+
+ public ObservableTextView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ObservableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ObservableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ if (changedView == this) {
+ if (mOnVisibilityChangedListener != null) {
+ mOnVisibilityChangedListener.accept(visibility);
+ }
+ }
+ }
+
+ public void setOnVisibilityChangedListener(Consumer<Integer> listener) {
+ mOnVisibilityChangedListener = listener;
+ }
+}
diff --git a/com/android/internal/widget/OpReorderer.java b/com/android/internal/widget/OpReorderer.java
new file mode 100644
index 0000000..babb087
--- /dev/null
+++ b/com/android/internal/widget/OpReorderer.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import static com.android.internal.widget.AdapterHelper.UpdateOp.ADD;
+import static com.android.internal.widget.AdapterHelper.UpdateOp.MOVE;
+import static com.android.internal.widget.AdapterHelper.UpdateOp.REMOVE;
+import static com.android.internal.widget.AdapterHelper.UpdateOp.UPDATE;
+
+import com.android.internal.widget.AdapterHelper.UpdateOp;
+
+import java.util.List;
+
+class OpReorderer {
+
+ final Callback mCallback;
+
+ OpReorderer(Callback callback) {
+ mCallback = callback;
+ }
+
+ void reorderOps(List<UpdateOp> ops) {
+ // since move operations breaks continuity, their effects on ADD/RM are hard to handle.
+ // we push them to the end of the list so that they can be handled easily.
+ int badMove;
+ while ((badMove = getLastMoveOutOfOrder(ops)) != -1) {
+ swapMoveOp(ops, badMove, badMove + 1);
+ }
+ }
+
+ private void swapMoveOp(List<UpdateOp> list, int badMove, int next) {
+ final UpdateOp moveOp = list.get(badMove);
+ final UpdateOp nextOp = list.get(next);
+ switch (nextOp.cmd) {
+ case REMOVE:
+ swapMoveRemove(list, badMove, moveOp, next, nextOp);
+ break;
+ case ADD:
+ swapMoveAdd(list, badMove, moveOp, next, nextOp);
+ break;
+ case UPDATE:
+ swapMoveUpdate(list, badMove, moveOp, next, nextOp);
+ break;
+ }
+ }
+
+ void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp,
+ int removePos, UpdateOp removeOp) {
+ UpdateOp extraRm = null;
+ // check if move is nulled out by remove
+ boolean revertedMove = false;
+ final boolean moveIsBackwards;
+
+ if (moveOp.positionStart < moveOp.itemCount) {
+ moveIsBackwards = false;
+ if (removeOp.positionStart == moveOp.positionStart
+ && removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
+ revertedMove = true;
+ }
+ } else {
+ moveIsBackwards = true;
+ if (removeOp.positionStart == moveOp.itemCount + 1
+ && removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
+ revertedMove = true;
+ }
+ }
+
+ // going in reverse, first revert the effect of add
+ if (moveOp.itemCount < removeOp.positionStart) {
+ removeOp.positionStart--;
+ } else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
+ // move is removed.
+ removeOp.itemCount--;
+ moveOp.cmd = REMOVE;
+ moveOp.itemCount = 1;
+ if (removeOp.itemCount == 0) {
+ list.remove(removePos);
+ mCallback.recycleUpdateOp(removeOp);
+ }
+ // no need to swap, it is already a remove
+ return;
+ }
+
+ // now affect of add is consumed. now apply effect of first remove
+ if (moveOp.positionStart <= removeOp.positionStart) {
+ removeOp.positionStart++;
+ } else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
+ final int remaining = removeOp.positionStart + removeOp.itemCount
+ - moveOp.positionStart;
+ extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining, null);
+ removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
+ }
+
+ // if effects of move is reverted by remove, we are done.
+ if (revertedMove) {
+ list.set(movePos, removeOp);
+ list.remove(removePos);
+ mCallback.recycleUpdateOp(moveOp);
+ return;
+ }
+
+ // now find out the new locations for move actions
+ if (moveIsBackwards) {
+ if (extraRm != null) {
+ if (moveOp.positionStart > extraRm.positionStart) {
+ moveOp.positionStart -= extraRm.itemCount;
+ }
+ if (moveOp.itemCount > extraRm.positionStart) {
+ moveOp.itemCount -= extraRm.itemCount;
+ }
+ }
+ if (moveOp.positionStart > removeOp.positionStart) {
+ moveOp.positionStart -= removeOp.itemCount;
+ }
+ if (moveOp.itemCount > removeOp.positionStart) {
+ moveOp.itemCount -= removeOp.itemCount;
+ }
+ } else {
+ if (extraRm != null) {
+ if (moveOp.positionStart >= extraRm.positionStart) {
+ moveOp.positionStart -= extraRm.itemCount;
+ }
+ if (moveOp.itemCount >= extraRm.positionStart) {
+ moveOp.itemCount -= extraRm.itemCount;
+ }
+ }
+ if (moveOp.positionStart >= removeOp.positionStart) {
+ moveOp.positionStart -= removeOp.itemCount;
+ }
+ if (moveOp.itemCount >= removeOp.positionStart) {
+ moveOp.itemCount -= removeOp.itemCount;
+ }
+ }
+
+ list.set(movePos, removeOp);
+ if (moveOp.positionStart != moveOp.itemCount) {
+ list.set(removePos, moveOp);
+ } else {
+ list.remove(removePos);
+ }
+ if (extraRm != null) {
+ list.add(movePos, extraRm);
+ }
+ }
+
+ private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add,
+ UpdateOp addOp) {
+ int offset = 0;
+ // going in reverse, first revert the effect of add
+ if (moveOp.itemCount < addOp.positionStart) {
+ offset--;
+ }
+ if (moveOp.positionStart < addOp.positionStart) {
+ offset++;
+ }
+ if (addOp.positionStart <= moveOp.positionStart) {
+ moveOp.positionStart += addOp.itemCount;
+ }
+ if (addOp.positionStart <= moveOp.itemCount) {
+ moveOp.itemCount += addOp.itemCount;
+ }
+ addOp.positionStart += offset;
+ list.set(move, addOp);
+ list.set(add, moveOp);
+ }
+
+ void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update,
+ UpdateOp updateOp) {
+ UpdateOp extraUp1 = null;
+ UpdateOp extraUp2 = null;
+ // going in reverse, first revert the effect of add
+ if (moveOp.itemCount < updateOp.positionStart) {
+ updateOp.positionStart--;
+ } else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
+ // moved item is updated. add an update for it
+ updateOp.itemCount--;
+ extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1, updateOp.payload);
+ }
+ // now affect of add is consumed. now apply effect of first remove
+ if (moveOp.positionStart <= updateOp.positionStart) {
+ updateOp.positionStart++;
+ } else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
+ final int remaining = updateOp.positionStart + updateOp.itemCount
+ - moveOp.positionStart;
+ extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining,
+ updateOp.payload);
+ updateOp.itemCount -= remaining;
+ }
+ list.set(update, moveOp);
+ if (updateOp.itemCount > 0) {
+ list.set(move, updateOp);
+ } else {
+ list.remove(move);
+ mCallback.recycleUpdateOp(updateOp);
+ }
+ if (extraUp1 != null) {
+ list.add(move, extraUp1);
+ }
+ if (extraUp2 != null) {
+ list.add(move, extraUp2);
+ }
+ }
+
+ private int getLastMoveOutOfOrder(List<UpdateOp> list) {
+ boolean foundNonMove = false;
+ for (int i = list.size() - 1; i >= 0; i--) {
+ final UpdateOp op1 = list.get(i);
+ if (op1.cmd == MOVE) {
+ if (foundNonMove) {
+ return i;
+ }
+ } else {
+ foundNonMove = true;
+ }
+ }
+ return -1;
+ }
+
+ interface Callback {
+
+ UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload);
+
+ void recycleUpdateOp(UpdateOp op);
+ }
+}
diff --git a/com/android/internal/widget/OrientationHelper.java b/com/android/internal/widget/OrientationHelper.java
new file mode 100644
index 0000000..1b02c88
--- /dev/null
+++ b/com/android/internal/widget/OrientationHelper.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.widget.LinearLayout;
+
+/**
+ * Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
+ * <p>
+ * It is developed to easily support vertical and horizontal orientations in a LayoutManager but
+ * can also be used to abstract calls around view bounds and child measurements with margins and
+ * decorations.
+ *
+ * @see #createHorizontalHelper(RecyclerView.LayoutManager)
+ * @see #createVerticalHelper(RecyclerView.LayoutManager)
+ */
+public abstract class OrientationHelper {
+
+ private static final int INVALID_SIZE = Integer.MIN_VALUE;
+
+ protected final RecyclerView.LayoutManager mLayoutManager;
+
+ public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+
+ public static final int VERTICAL = LinearLayout.VERTICAL;
+
+ private int mLastTotalSpace = INVALID_SIZE;
+
+ final Rect mTmpRect = new Rect();
+
+ private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
+ mLayoutManager = layoutManager;
+ }
+
+ /**
+ * Call this method after onLayout method is complete if state is NOT pre-layout.
+ * This method records information like layout bounds that might be useful in the next layout
+ * calculations.
+ */
+ public void onLayoutComplete() {
+ mLastTotalSpace = getTotalSpace();
+ }
+
+ /**
+ * Returns the layout space change between the previous layout pass and current layout pass.
+ * <p>
+ * Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's
+ * {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler,
+ * RecyclerView.State)} method.
+ *
+ * @return The difference between the current total space and previous layout's total space.
+ * @see #onLayoutComplete()
+ */
+ public int getTotalSpaceChange() {
+ return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace;
+ }
+
+ /**
+ * Returns the start of the view including its decoration and margin.
+ * <p>
+ * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left
+ * decoration and 3px left margin, returned value will be 15px.
+ *
+ * @param view The view element to check
+ * @return The first pixel of the element
+ * @see #getDecoratedEnd(android.view.View)
+ */
+ public abstract int getDecoratedStart(View view);
+
+ /**
+ * Returns the end of the view including its decoration and margin.
+ * <p>
+ * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
+ * decoration and 3px right margin, returned value will be 205.
+ *
+ * @param view The view element to check
+ * @return The last pixel of the element
+ * @see #getDecoratedStart(android.view.View)
+ */
+ public abstract int getDecoratedEnd(View view);
+
+ /**
+ * Returns the end of the View after its matrix transformations are applied to its layout
+ * position.
+ * <p>
+ * This method is useful when trying to detect the visible edge of a View.
+ * <p>
+ * It includes the decorations but does not include the margins.
+ *
+ * @param view The view whose transformed end will be returned
+ * @return The end of the View after its decor insets and transformation matrix is applied to
+ * its position
+ *
+ * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect)
+ */
+ public abstract int getTransformedEndWithDecoration(View view);
+
+ /**
+ * Returns the start of the View after its matrix transformations are applied to its layout
+ * position.
+ * <p>
+ * This method is useful when trying to detect the visible edge of a View.
+ * <p>
+ * It includes the decorations but does not include the margins.
+ *
+ * @param view The view whose transformed start will be returned
+ * @return The start of the View after its decor insets and transformation matrix is applied to
+ * its position
+ *
+ * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect)
+ */
+ public abstract int getTransformedStartWithDecoration(View view);
+
+ /**
+ * Returns the space occupied by this View in the current orientation including decorations and
+ * margins.
+ *
+ * @param view The view element to check
+ * @return Total space occupied by this view
+ * @see #getDecoratedMeasurementInOther(View)
+ */
+ public abstract int getDecoratedMeasurement(View view);
+
+ /**
+ * Returns the space occupied by this View in the perpendicular orientation including
+ * decorations and margins.
+ *
+ * @param view The view element to check
+ * @return Total space occupied by this view in the perpendicular orientation to current one
+ * @see #getDecoratedMeasurement(View)
+ */
+ public abstract int getDecoratedMeasurementInOther(View view);
+
+ /**
+ * Returns the start position of the layout after the start padding is added.
+ *
+ * @return The very first pixel we can draw.
+ */
+ public abstract int getStartAfterPadding();
+
+ /**
+ * Returns the end position of the layout after the end padding is removed.
+ *
+ * @return The end boundary for this layout.
+ */
+ public abstract int getEndAfterPadding();
+
+ /**
+ * Returns the end position of the layout without taking padding into account.
+ *
+ * @return The end boundary for this layout without considering padding.
+ */
+ public abstract int getEnd();
+
+ /**
+ * Offsets all children's positions by the given amount.
+ *
+ * @param amount Value to add to each child's layout parameters
+ */
+ public abstract void offsetChildren(int amount);
+
+ /**
+ * Returns the total space to layout. This number is the difference between
+ * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
+ *
+ * @return Total space to layout children
+ */
+ public abstract int getTotalSpace();
+
+ /**
+ * Offsets the child in this orientation.
+ *
+ * @param view View to offset
+ * @param offset offset amount
+ */
+ public abstract void offsetChild(View view, int offset);
+
+ /**
+ * Returns the padding at the end of the layout. For horizontal helper, this is the right
+ * padding and for vertical helper, this is the bottom padding. This method does not check
+ * whether the layout is RTL or not.
+ *
+ * @return The padding at the end of the layout.
+ */
+ public abstract int getEndPadding();
+
+ /**
+ * Returns the MeasureSpec mode for the current orientation from the LayoutManager.
+ *
+ * @return The current measure spec mode.
+ *
+ * @see View.MeasureSpec
+ * @see RecyclerView.LayoutManager#getWidthMode()
+ * @see RecyclerView.LayoutManager#getHeightMode()
+ */
+ public abstract int getMode();
+
+ /**
+ * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager.
+ *
+ * @return The current measure spec mode.
+ *
+ * @see View.MeasureSpec
+ * @see RecyclerView.LayoutManager#getWidthMode()
+ * @see RecyclerView.LayoutManager#getHeightMode()
+ */
+ public abstract int getModeInOther();
+
+ /**
+ * Creates an OrientationHelper for the given LayoutManager and orientation.
+ *
+ * @param layoutManager LayoutManager to attach to
+ * @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createOrientationHelper(
+ RecyclerView.LayoutManager layoutManager, int orientation) {
+ switch (orientation) {
+ case HORIZONTAL:
+ return createHorizontalHelper(layoutManager);
+ case VERTICAL:
+ return createVerticalHelper(layoutManager);
+ }
+ throw new IllegalArgumentException("invalid orientation");
+ }
+
+ /**
+ * Creates a horizontal OrientationHelper for the given LayoutManager.
+ *
+ * @param layoutManager The LayoutManager to attach to.
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createHorizontalHelper(
+ RecyclerView.LayoutManager layoutManager) {
+ return new OrientationHelper(layoutManager) {
+ @Override
+ public int getEndAfterPadding() {
+ return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public int getEnd() {
+ return mLayoutManager.getWidth();
+ }
+
+ @Override
+ public void offsetChildren(int amount) {
+ mLayoutManager.offsetChildrenHorizontal(amount);
+ }
+
+ @Override
+ public int getStartAfterPadding() {
+ return mLayoutManager.getPaddingLeft();
+ }
+
+ @Override
+ public int getDecoratedMeasurement(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedMeasurementInOther(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedEnd(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedStart(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
+ }
+
+ @Override
+ public int getTransformedEndWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.right;
+ }
+
+ @Override
+ public int getTransformedStartWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.left;
+ }
+
+ @Override
+ public int getTotalSpace() {
+ return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
+ - mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public void offsetChild(View view, int offset) {
+ view.offsetLeftAndRight(offset);
+ }
+
+ @Override
+ public int getEndPadding() {
+ return mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public int getMode() {
+ return mLayoutManager.getWidthMode();
+ }
+
+ @Override
+ public int getModeInOther() {
+ return mLayoutManager.getHeightMode();
+ }
+ };
+ }
+
+ /**
+ * Creates a vertical OrientationHelper for the given LayoutManager.
+ *
+ * @param layoutManager The LayoutManager to attach to.
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
+ return new OrientationHelper(layoutManager) {
+ @Override
+ public int getEndAfterPadding() {
+ return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public int getEnd() {
+ return mLayoutManager.getHeight();
+ }
+
+ @Override
+ public void offsetChildren(int amount) {
+ mLayoutManager.offsetChildrenVertical(amount);
+ }
+
+ @Override
+ public int getStartAfterPadding() {
+ return mLayoutManager.getPaddingTop();
+ }
+
+ @Override
+ public int getDecoratedMeasurement(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedMeasurementInOther(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedEnd(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedStart(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedTop(view) - params.topMargin;
+ }
+
+ @Override
+ public int getTransformedEndWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.bottom;
+ }
+
+ @Override
+ public int getTransformedStartWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.top;
+ }
+
+ @Override
+ public int getTotalSpace() {
+ return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
+ - mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public void offsetChild(View view, int offset) {
+ view.offsetTopAndBottom(offset);
+ }
+
+ @Override
+ public int getEndPadding() {
+ return mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public int getMode() {
+ return mLayoutManager.getHeightMode();
+ }
+
+ @Override
+ public int getModeInOther() {
+ return mLayoutManager.getWidthMode();
+ }
+ };
+ }
+}
diff --git a/com/android/internal/widget/PagerAdapter.java b/com/android/internal/widget/PagerAdapter.java
new file mode 100644
index 0000000..910a720
--- /dev/null
+++ b/com/android/internal/widget/PagerAdapter.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Base class providing the adapter to populate pages inside of
+ * a {@link android.support.v4.view.ViewPager}. You will most likely want to use a more
+ * specific implementation of this, such as
+ * {@link android.support.v4.app.FragmentPagerAdapter} or
+ * {@link android.support.v4.app.FragmentStatePagerAdapter}.
+ *
+ * <p>When you implement a PagerAdapter, you must override the following methods
+ * at minimum:</p>
+ * <ul>
+ * <li>{@link #instantiateItem(android.view.ViewGroup, int)}</li>
+ * <li>{@link #destroyItem(android.view.ViewGroup, int, Object)}</li>
+ * <li>{@link #getCount()}</li>
+ * <li>{@link #isViewFromObject(android.view.View, Object)}</li>
+ * </ul>
+ *
+ * <p>PagerAdapter is more general than the adapters used for
+ * {@link android.widget.AdapterView AdapterViews}. Instead of providing a
+ * View recycling mechanism directly ViewPager uses callbacks to indicate the
+ * steps taken during an update. A PagerAdapter may implement a form of View
+ * recycling if desired or use a more sophisticated method of managing page
+ * Views such as Fragment transactions where each page is represented by its
+ * own Fragment.</p>
+ *
+ * <p>ViewPager associates each page with a key Object instead of working with
+ * Views directly. This key is used to track and uniquely identify a given page
+ * independent of its position in the adapter. A call to the PagerAdapter method
+ * {@link #startUpdate(android.view.ViewGroup)} indicates that the contents of the ViewPager
+ * are about to change. One or more calls to {@link #instantiateItem(android.view.ViewGroup, int)}
+ * and/or {@link #destroyItem(android.view.ViewGroup, int, Object)} will follow, and the end
+ * of an update will be signaled by a call to {@link #finishUpdate(android.view.ViewGroup)}.
+ * By the time {@link #finishUpdate(android.view.ViewGroup) finishUpdate} returns the views
+ * associated with the key objects returned by
+ * {@link #instantiateItem(android.view.ViewGroup, int) instantiateItem} should be added to
+ * the parent ViewGroup passed to these methods and the views associated with
+ * the keys passed to {@link #destroyItem(android.view.ViewGroup, int, Object) destroyItem}
+ * should be removed. The method {@link #isViewFromObject(android.view.View, Object)} identifies
+ * whether a page View is associated with a given key object.</p>
+ *
+ * <p>A very simple PagerAdapter may choose to use the page Views themselves
+ * as key objects, returning them from {@link #instantiateItem(android.view.ViewGroup, int)}
+ * after creation and adding them to the parent ViewGroup. A matching
+ * {@link #destroyItem(android.view.ViewGroup, int, Object)} implementation would remove the
+ * View from the parent ViewGroup and {@link #isViewFromObject(android.view.View, Object)}
+ * could be implemented as <code>return view == object;</code>.</p>
+ *
+ * <p>PagerAdapter supports data set changes. Data set changes must occur on the
+ * main thread and must end with a call to {@link #notifyDataSetChanged()} similar
+ * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data
+ * set change may involve pages being added, removed, or changing position. The
+ * ViewPager will keep the current page active provided the adapter implements
+ * the method {@link #getItemPosition(Object)}.</p>
+ */
+public abstract class PagerAdapter {
+ private DataSetObservable mObservable = new DataSetObservable();
+
+ public static final int POSITION_UNCHANGED = -1;
+ public static final int POSITION_NONE = -2;
+
+ /**
+ * Return the number of views available.
+ */
+ public abstract int getCount();
+
+ /**
+ * Called when a change in the shown pages is going to start being made.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ */
+ public void startUpdate(ViewGroup container) {
+ startUpdate((View) container);
+ }
+
+ /**
+ * Create the page for the given position. The adapter is responsible
+ * for adding the view to the container given here, although it only
+ * must ensure this is done by the time it returns from
+ * {@link #finishUpdate(android.view.ViewGroup)}.
+ *
+ * @param container The containing View in which the page will be shown.
+ * @param position The page position to be instantiated.
+ * @return Returns an Object representing the new page. This does not
+ * need to be a View, but can be some other container of the page.
+ */
+ public Object instantiateItem(ViewGroup container, int position) {
+ return instantiateItem((View) container, position);
+ }
+
+ /**
+ * Remove a page for the given position. The adapter is responsible
+ * for removing the view from its container, although it only must ensure
+ * this is done by the time it returns from {@link #finishUpdate(android.view.ViewGroup)}.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position to be removed.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(android.view.View, int)}.
+ */
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ destroyItem((View) container, position, object);
+ }
+
+ /**
+ * Called to inform the adapter of which item is currently considered to
+ * be the "primary", that is the one show to the user as the current page.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position that is now the primary.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(android.view.View, int)}.
+ */
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ setPrimaryItem((View) container, position, object);
+ }
+
+ /**
+ * Called when the a change in the shown pages has been completed. At this
+ * point you must ensure that all of the pages have actually been added or
+ * removed from the container as appropriate.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ */
+ public void finishUpdate(ViewGroup container) {
+ finishUpdate((View) container);
+ }
+
+ /**
+ * Called when a change in the shown pages is going to start being made.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ *
+ * @deprecated Use {@link #startUpdate(android.view.ViewGroup)}
+ */
+ public void startUpdate(View container) {
+ }
+
+ /**
+ * Create the page for the given position. The adapter is responsible
+ * for adding the view to the container given here, although it only
+ * must ensure this is done by the time it returns from
+ * {@link #finishUpdate(android.view.ViewGroup)}.
+ *
+ * @param container The containing View in which the page will be shown.
+ * @param position The page position to be instantiated.
+ * @return Returns an Object representing the new page. This does not
+ * need to be a View, but can be some other container of the page.
+ *
+ * @deprecated Use {@link #instantiateItem(android.view.ViewGroup, int)}
+ */
+ public Object instantiateItem(View container, int position) {
+ throw new UnsupportedOperationException(
+ "Required method instantiateItem was not overridden");
+ }
+
+ /**
+ * Remove a page for the given position. The adapter is responsible
+ * for removing the view from its container, although it only must ensure
+ * this is done by the time it returns from {@link #finishUpdate(android.view.View)}.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position to be removed.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(android.view.View, int)}.
+ *
+ * @deprecated Use {@link #destroyItem(android.view.ViewGroup, int, Object)}
+ */
+ public void destroyItem(View container, int position, Object object) {
+ throw new UnsupportedOperationException("Required method destroyItem was not overridden");
+ }
+
+ /**
+ * Called to inform the adapter of which item is currently considered to
+ * be the "primary", that is the one show to the user as the current page.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position that is now the primary.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(android.view.View, int)}.
+ *
+ * @deprecated Use {@link #setPrimaryItem(android.view.ViewGroup, int, Object)}
+ */
+ public void setPrimaryItem(View container, int position, Object object) {
+ }
+
+ /**
+ * Called when the a change in the shown pages has been completed. At this
+ * point you must ensure that all of the pages have actually been added or
+ * removed from the container as appropriate.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ *
+ * @deprecated Use {@link #finishUpdate(android.view.ViewGroup)}
+ */
+ public void finishUpdate(View container) {
+ }
+
+ /**
+ * Determines whether a page View is associated with a specific key object
+ * as returned by {@link #instantiateItem(android.view.ViewGroup, int)}. This method is
+ * required for a PagerAdapter to function properly.
+ *
+ * @param view Page View to check for association with <code>object</code>
+ * @param object Object to check for association with <code>view</code>
+ * @return true if <code>view</code> is associated with the key object <code>object</code>
+ */
+ public abstract boolean isViewFromObject(View view, Object object);
+
+ /**
+ * Save any instance state associated with this adapter and its pages that should be
+ * restored if the current UI state needs to be reconstructed.
+ *
+ * @return Saved state for this adapter
+ */
+ public Parcelable saveState() {
+ return null;
+ }
+
+ /**
+ * Restore any instance state associated with this adapter and its pages
+ * that was previously saved by {@link #saveState()}.
+ *
+ * @param state State previously saved by a call to {@link #saveState()}
+ * @param loader A ClassLoader that should be used to instantiate any restored objects
+ */
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ }
+
+ /**
+ * Called when the host view is attempting to determine if an item's position
+ * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
+ * item has not changed or {@link #POSITION_NONE} if the item is no longer present
+ * in the adapter.
+ *
+ * <p>The default implementation assumes that items will never
+ * change position and always returns {@link #POSITION_UNCHANGED}.
+ *
+ * @param object Object representing an item, previously returned by a call to
+ * {@link #instantiateItem(android.view.View, int)}.
+ * @return object's new position index from [0, {@link #getCount()}),
+ * {@link #POSITION_UNCHANGED} if the object's position has not changed,
+ * or {@link #POSITION_NONE} if the item is no longer present.
+ */
+ public int getItemPosition(Object object) {
+ return POSITION_UNCHANGED;
+ }
+
+ /**
+ * This method should be called by the application if the data backing this adapter has changed
+ * and associated views should update.
+ */
+ public void notifyDataSetChanged() {
+ mObservable.notifyChanged();
+ }
+
+ /**
+ * Register an observer to receive callbacks related to the adapter's data changing.
+ *
+ * @param observer The {@link android.database.DataSetObserver} which will receive callbacks.
+ */
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mObservable.registerObserver(observer);
+ }
+
+ /**
+ * Unregister an observer from callbacks related to the adapter's data changing.
+ *
+ * @param observer The {@link android.database.DataSetObserver} which will be unregistered.
+ */
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * This method may be called by the ViewPager to obtain a title string
+ * to describe the specified page. This method may return null
+ * indicating no title for this page. The default implementation returns
+ * null.
+ *
+ * @param position The position of the title requested
+ * @return A title for the requested page
+ */
+ public CharSequence getPageTitle(int position) {
+ return null;
+ }
+
+ /**
+ * Returns the proportional width of a given page as a percentage of the
+ * ViewPager's measured width from (0.f-1.f]
+ *
+ * @param position The position of the page requested
+ * @return Proportional width for the given page position
+ */
+ public float getPageWidth(int position) {
+ return 1.f;
+ }
+}
diff --git a/com/android/internal/widget/PasswordValidationError.java b/com/android/internal/widget/PasswordValidationError.java
new file mode 100644
index 0000000..41b234e
--- /dev/null
+++ b/com/android/internal/widget/PasswordValidationError.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 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.internal.widget;
+
+/**
+ * Password validation error containing an error code and optional requirement.
+ */
+public class PasswordValidationError {
+ // Password validation error codes
+ public static final int WEAK_CREDENTIAL_TYPE = 1;
+ public static final int CONTAINS_INVALID_CHARACTERS = 2;
+ public static final int TOO_SHORT = 3;
+ public static final int TOO_LONG = 4;
+ public static final int CONTAINS_SEQUENCE = 5;
+ public static final int NOT_ENOUGH_LETTERS = 6;
+ public static final int NOT_ENOUGH_UPPER_CASE = 7;
+ public static final int NOT_ENOUGH_LOWER_CASE = 8;
+ public static final int NOT_ENOUGH_DIGITS = 9;
+ public static final int NOT_ENOUGH_SYMBOLS = 10;
+ public static final int NOT_ENOUGH_NON_LETTER = 11;
+ public static final int NOT_ENOUGH_NON_DIGITS = 12;
+ public static final int RECENTLY_USED = 13;
+ // WARNING: if you add a new error, make sure it is presented to the user correctly in Settings.
+
+ public final int errorCode;
+ public final int requirement;
+
+ public PasswordValidationError(int errorCode) {
+ this(errorCode, 0);
+ }
+
+ public PasswordValidationError(int errorCode, int requirement) {
+ this.errorCode = errorCode;
+ this.requirement = requirement;
+ }
+
+ @Override
+ public String toString() {
+ return errorCodeToString(errorCode) + (requirement > 0 ? "; required: " + requirement : "");
+ }
+
+ /**
+ * Returns textual representation of the error for logging purposes.
+ */
+ private static String errorCodeToString(int error) {
+ switch (error) {
+ case WEAK_CREDENTIAL_TYPE: return "Weak credential type";
+ case CONTAINS_INVALID_CHARACTERS: return "Contains an invalid character";
+ case TOO_SHORT: return "Password too short";
+ case TOO_LONG: return "Password too long";
+ case CONTAINS_SEQUENCE: return "Sequence too long";
+ case NOT_ENOUGH_LETTERS: return "Too few letters";
+ case NOT_ENOUGH_UPPER_CASE: return "Too few upper case letters";
+ case NOT_ENOUGH_LOWER_CASE: return "Too few lower case letters";
+ case NOT_ENOUGH_DIGITS: return "Too few numeric characters";
+ case NOT_ENOUGH_SYMBOLS: return "Too few symbols";
+ case NOT_ENOUGH_NON_LETTER: return "Too few non-letter characters";
+ case NOT_ENOUGH_NON_DIGITS: return "Too few non-numeric characters";
+ case RECENTLY_USED: return "Pin or password was recently used";
+ default: return "Unknown error " + error;
+ }
+ }
+
+}
diff --git a/com/android/internal/widget/PointerLocationView.java b/com/android/internal/widget/PointerLocationView.java
new file mode 100644
index 0000000..dc8d57a
--- /dev/null
+++ b/com/android/internal/widget/PointerLocationView.java
@@ -0,0 +1,980 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.hardware.input.InputManager;
+import android.hardware.input.InputManager.InputDeviceListener;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.util.Slog;
+import android.view.ISystemGestureExclusionListener;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.WindowInsets;
+import android.view.WindowManagerGlobal;
+import android.view.WindowManagerPolicyConstants.PointerEventListener;
+
+import java.util.ArrayList;
+
+public class PointerLocationView extends View implements InputDeviceListener,
+ PointerEventListener {
+ private static final String TAG = "Pointer";
+
+ // The system property key used to specify an alternate velocity tracker strategy
+ // to plot alongside the default one. Useful for testing and comparison purposes.
+ private static final String ALT_STRATEGY_PROPERY_KEY = "debug.velocitytracker.alt";
+
+ /**
+ * If set to a positive value between 1-255, shows an overlay with the approved (red) and
+ * rejected (blue) exclusions.
+ */
+ private static final String GESTURE_EXCLUSION_PROP = "debug.pointerlocation.showexclusion";
+
+ public static class PointerState {
+ // Trace of previous points.
+ private float[] mTraceX = new float[32];
+ private float[] mTraceY = new float[32];
+ private boolean[] mTraceCurrent = new boolean[32];
+ private int mTraceCount;
+
+ // True if the pointer is down.
+ @UnsupportedAppUsage
+ private boolean mCurDown;
+
+ // Most recent coordinates.
+ private PointerCoords mCoords = new PointerCoords();
+ private int mToolType;
+
+ // Most recent velocity.
+ private float mXVelocity;
+ private float mYVelocity;
+ private float mAltXVelocity;
+ private float mAltYVelocity;
+
+ // Current bounding box, if any
+ private boolean mHasBoundingBox;
+ private float mBoundingLeft;
+ private float mBoundingTop;
+ private float mBoundingRight;
+ private float mBoundingBottom;
+
+ // Position estimator.
+ private VelocityTracker.Estimator mEstimator = new VelocityTracker.Estimator();
+ private VelocityTracker.Estimator mAltEstimator = new VelocityTracker.Estimator();
+
+ @UnsupportedAppUsage
+ public PointerState() {
+ }
+
+ public void clearTrace() {
+ mTraceCount = 0;
+ }
+
+ public void addTrace(float x, float y, boolean current) {
+ int traceCapacity = mTraceX.length;
+ if (mTraceCount == traceCapacity) {
+ traceCapacity *= 2;
+ float[] newTraceX = new float[traceCapacity];
+ System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
+ mTraceX = newTraceX;
+
+ float[] newTraceY = new float[traceCapacity];
+ System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
+ mTraceY = newTraceY;
+
+ boolean[] newTraceCurrent = new boolean[traceCapacity];
+ System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount);
+ mTraceCurrent= newTraceCurrent;
+ }
+
+ mTraceX[mTraceCount] = x;
+ mTraceY[mTraceCount] = y;
+ mTraceCurrent[mTraceCount] = current;
+ mTraceCount += 1;
+ }
+ }
+
+ private final InputManager mIm;
+
+ private final ViewConfiguration mVC;
+ private final Paint mTextPaint;
+ private final Paint mTextBackgroundPaint;
+ private final Paint mTextLevelPaint;
+ private final Paint mPaint;
+ private final Paint mCurrentPointPaint;
+ private final Paint mTargetPaint;
+ private final Paint mPathPaint;
+ private final FontMetricsInt mTextMetrics = new FontMetricsInt();
+ private int mHeaderBottom;
+ private int mHeaderPaddingTop = 0;
+ @UnsupportedAppUsage
+ private boolean mCurDown;
+ @UnsupportedAppUsage
+ private int mCurNumPointers;
+ @UnsupportedAppUsage
+ private int mMaxNumPointers;
+ private int mActivePointerId;
+ @UnsupportedAppUsage
+ private final ArrayList<PointerState> mPointers = new ArrayList<PointerState>();
+ private final PointerCoords mTempCoords = new PointerCoords();
+
+ private final Region mSystemGestureExclusion = new Region();
+ private final Region mSystemGestureExclusionRejected = new Region();
+ private final Path mSystemGestureExclusionPath = new Path();
+ private final Paint mSystemGestureExclusionPaint;
+ private final Paint mSystemGestureExclusionRejectedPaint;
+
+ private final VelocityTracker mVelocity;
+ private final VelocityTracker mAltVelocity;
+
+ private final FasterStringBuilder mText = new FasterStringBuilder();
+
+ @UnsupportedAppUsage
+ private boolean mPrintCoords = true;
+
+ public PointerLocationView(Context c) {
+ super(c);
+ setFocusableInTouchMode(true);
+
+ mIm = c.getSystemService(InputManager.class);
+
+ mVC = ViewConfiguration.get(c);
+ mTextPaint = new Paint();
+ mTextPaint.setAntiAlias(true);
+ mTextPaint.setTextSize(10
+ * getResources().getDisplayMetrics().density);
+ mTextPaint.setARGB(255, 0, 0, 0);
+ mTextBackgroundPaint = new Paint();
+ mTextBackgroundPaint.setAntiAlias(false);
+ mTextBackgroundPaint.setARGB(128, 255, 255, 255);
+ mTextLevelPaint = new Paint();
+ mTextLevelPaint.setAntiAlias(false);
+ mTextLevelPaint.setARGB(192, 255, 0, 0);
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setARGB(255, 255, 255, 255);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(2);
+ mCurrentPointPaint = new Paint();
+ mCurrentPointPaint.setAntiAlias(true);
+ mCurrentPointPaint.setARGB(255, 255, 0, 0);
+ mCurrentPointPaint.setStyle(Paint.Style.STROKE);
+ mCurrentPointPaint.setStrokeWidth(2);
+ mTargetPaint = new Paint();
+ mTargetPaint.setAntiAlias(false);
+ mTargetPaint.setARGB(255, 0, 0, 192);
+ mPathPaint = new Paint();
+ mPathPaint.setAntiAlias(false);
+ mPathPaint.setARGB(255, 0, 96, 255);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(1);
+
+ mSystemGestureExclusionPaint = new Paint();
+ mSystemGestureExclusionPaint.setARGB(25, 255, 0, 0);
+ mSystemGestureExclusionPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+
+ mSystemGestureExclusionRejectedPaint = new Paint();
+ mSystemGestureExclusionRejectedPaint.setARGB(25, 0, 0, 255);
+ mSystemGestureExclusionRejectedPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+
+ PointerState ps = new PointerState();
+ mPointers.add(ps);
+ mActivePointerId = 0;
+
+ mVelocity = VelocityTracker.obtain();
+
+ String altStrategy = SystemProperties.get(ALT_STRATEGY_PROPERY_KEY);
+ if (altStrategy.length() != 0) {
+ Log.d(TAG, "Comparing default velocity tracker strategy with " + altStrategy);
+ mAltVelocity = VelocityTracker.obtain(altStrategy);
+ } else {
+ mAltVelocity = null;
+ }
+ }
+
+ public void setPrintCoords(boolean state) {
+ mPrintCoords = state;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (insets.getDisplayCutout() != null) {
+ mHeaderPaddingTop = insets.getDisplayCutout().getSafeInsetTop();
+ } else {
+ mHeaderPaddingTop = 0;
+ }
+ return super.onApplyWindowInsets(insets);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mTextPaint.getFontMetricsInt(mTextMetrics);
+ mHeaderBottom = mHeaderPaddingTop-mTextMetrics.ascent+mTextMetrics.descent+2;
+ if (false) {
+ Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
+ + " descent=" + mTextMetrics.descent
+ + " leading=" + mTextMetrics.leading
+ + " top=" + mTextMetrics.top
+ + " bottom=" + mTextMetrics.bottom);
+ }
+ }
+
+ // Draw an oval. When angle is 0 radians, orients the major axis vertically,
+ // angles less than or greater than 0 radians rotate the major axis left or right.
+ private RectF mReusableOvalRect = new RectF();
+ private void drawOval(Canvas canvas, float x, float y, float major, float minor,
+ float angle, Paint paint) {
+ canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ canvas.rotate((float) (angle * 180 / Math.PI), x, y);
+ mReusableOvalRect.left = x - minor / 2;
+ mReusableOvalRect.right = x + minor / 2;
+ mReusableOvalRect.top = y - major / 2;
+ mReusableOvalRect.bottom = y + major / 2;
+ canvas.drawOval(mReusableOvalRect, paint);
+ canvas.restore();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int w = getWidth();
+ final int itemW = w/7;
+ final int base = mHeaderPaddingTop-mTextMetrics.ascent+1;
+ final int bottom = mHeaderBottom;
+
+ final int NP = mPointers.size();
+
+ if (!mSystemGestureExclusion.isEmpty()) {
+ mSystemGestureExclusionPath.reset();
+ mSystemGestureExclusion.getBoundaryPath(mSystemGestureExclusionPath);
+ canvas.drawPath(mSystemGestureExclusionPath, mSystemGestureExclusionPaint);
+ }
+
+ if (!mSystemGestureExclusionRejected.isEmpty()) {
+ mSystemGestureExclusionPath.reset();
+ mSystemGestureExclusionRejected.getBoundaryPath(mSystemGestureExclusionPath);
+ canvas.drawPath(mSystemGestureExclusionPath, mSystemGestureExclusionRejectedPaint);
+ }
+
+ // Labels
+ if (mActivePointerId >= 0) {
+ final PointerState ps = mPointers.get(mActivePointerId);
+
+ canvas.drawRect(0, mHeaderPaddingTop, itemW-1, bottom,mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("P: ").append(mCurNumPointers)
+ .append(" / ").append(mMaxNumPointers)
+ .toString(), 1, base, mTextPaint);
+
+ final int N = ps.mTraceCount;
+ if ((mCurDown && ps.mCurDown) || N == 0) {
+ canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
+ mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("X: ").append(ps.mCoords.x, 1)
+ .toString(), 1 + itemW, base, mTextPaint);
+ canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
+ mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("Y: ").append(ps.mCoords.y, 1)
+ .toString(), 1 + itemW * 2, base, mTextPaint);
+ } else {
+ float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
+ float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
+ canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
+ Math.abs(dx) < mVC.getScaledTouchSlop()
+ ? mTextBackgroundPaint : mTextLevelPaint);
+ canvas.drawText(mText.clear()
+ .append("dX: ").append(dx, 1)
+ .toString(), 1 + itemW, base, mTextPaint);
+ canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
+ Math.abs(dy) < mVC.getScaledTouchSlop()
+ ? mTextBackgroundPaint : mTextLevelPaint);
+ canvas.drawText(mText.clear()
+ .append("dY: ").append(dy, 1)
+ .toString(), 1 + itemW * 2, base, mTextPaint);
+ }
+
+ canvas.drawRect(itemW * 3, mHeaderPaddingTop, (itemW * 4) - 1, bottom,
+ mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("Xv: ").append(ps.mXVelocity, 3)
+ .toString(), 1 + itemW * 3, base, mTextPaint);
+
+ canvas.drawRect(itemW * 4, mHeaderPaddingTop, (itemW * 5) - 1, bottom,
+ mTextBackgroundPaint);
+ canvas.drawText(mText.clear()
+ .append("Yv: ").append(ps.mYVelocity, 3)
+ .toString(), 1 + itemW * 4, base, mTextPaint);
+
+ canvas.drawRect(itemW * 5, mHeaderPaddingTop, (itemW * 6) - 1, bottom,
+ mTextBackgroundPaint);
+ canvas.drawRect(itemW * 5, mHeaderPaddingTop,
+ (itemW * 5) + (ps.mCoords.pressure * itemW) - 1, bottom, mTextLevelPaint);
+ canvas.drawText(mText.clear()
+ .append("Prs: ").append(ps.mCoords.pressure, 2)
+ .toString(), 1 + itemW * 5, base, mTextPaint);
+
+ canvas.drawRect(itemW * 6, mHeaderPaddingTop, w, bottom, mTextBackgroundPaint);
+ canvas.drawRect(itemW * 6, mHeaderPaddingTop,
+ (itemW * 6) + (ps.mCoords.size * itemW) - 1, bottom, mTextLevelPaint);
+ canvas.drawText(mText.clear()
+ .append("Size: ").append(ps.mCoords.size, 2)
+ .toString(), 1 + itemW * 6, base, mTextPaint);
+ }
+
+ // Pointer trace.
+ for (int p = 0; p < NP; p++) {
+ final PointerState ps = mPointers.get(p);
+
+ // Draw path.
+ final int N = ps.mTraceCount;
+ float lastX = 0, lastY = 0;
+ boolean haveLast = false;
+ boolean drawn = false;
+ mPaint.setARGB(255, 128, 255, 255);
+ for (int i=0; i < N; i++) {
+ float x = ps.mTraceX[i];
+ float y = ps.mTraceY[i];
+ if (Float.isNaN(x)) {
+ haveLast = false;
+ continue;
+ }
+ if (haveLast) {
+ canvas.drawLine(lastX, lastY, x, y, mPathPaint);
+ final Paint paint = ps.mTraceCurrent[i] ? mCurrentPointPaint : mPaint;
+ canvas.drawPoint(lastX, lastY, paint);
+ drawn = true;
+ }
+ lastX = x;
+ lastY = y;
+ haveLast = true;
+ }
+
+ if (drawn) {
+ // Draw velocity vector.
+ mPaint.setARGB(255, 255, 64, 128);
+ float xVel = ps.mXVelocity * (1000 / 60);
+ float yVel = ps.mYVelocity * (1000 / 60);
+ canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
+
+ // Draw velocity vector using an alternate VelocityTracker strategy.
+ if (mAltVelocity != null) {
+ mPaint.setARGB(255, 64, 255, 128);
+ xVel = ps.mAltXVelocity * (1000 / 60);
+ yVel = ps.mAltYVelocity * (1000 / 60);
+ canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
+ }
+ }
+
+ if (mCurDown && ps.mCurDown) {
+ // Draw crosshairs.
+ canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
+ canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
+
+ // Draw current point.
+ int pressureLevel = (int)(ps.mCoords.pressure * 255);
+ mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
+ canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
+
+ // Draw current touch ellipse.
+ mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
+ drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
+ ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
+
+ // Draw current tool ellipse.
+ mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
+ drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
+ ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
+
+ // Draw the orientation arrow.
+ float arrowSize = ps.mCoords.toolMajor * 0.7f;
+ if (arrowSize < 20) {
+ arrowSize = 20;
+ }
+ mPaint.setARGB(255, pressureLevel, 255, 0);
+ float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
+ * arrowSize);
+ float orientationVectorY = (float) (-Math.cos(ps.mCoords.orientation)
+ * arrowSize);
+ if (ps.mToolType == MotionEvent.TOOL_TYPE_STYLUS
+ || ps.mToolType == MotionEvent.TOOL_TYPE_ERASER) {
+ // Show full circle orientation.
+ canvas.drawLine(ps.mCoords.x, ps.mCoords.y,
+ ps.mCoords.x + orientationVectorX,
+ ps.mCoords.y + orientationVectorY,
+ mPaint);
+ } else {
+ // Show half circle orientation.
+ canvas.drawLine(
+ ps.mCoords.x - orientationVectorX,
+ ps.mCoords.y - orientationVectorY,
+ ps.mCoords.x + orientationVectorX,
+ ps.mCoords.y + orientationVectorY,
+ mPaint);
+ }
+
+ // Draw the tilt point along the orientation arrow.
+ float tiltScale = (float) Math.sin(
+ ps.mCoords.getAxisValue(MotionEvent.AXIS_TILT));
+ canvas.drawCircle(
+ ps.mCoords.x + orientationVectorX * tiltScale,
+ ps.mCoords.y + orientationVectorY * tiltScale,
+ 3.0f, mPaint);
+
+ // Draw the current bounding box
+ if (ps.mHasBoundingBox) {
+ canvas.drawRect(ps.mBoundingLeft, ps.mBoundingTop,
+ ps.mBoundingRight, ps.mBoundingBottom, mPaint);
+ }
+ }
+ }
+ }
+
+ private void logMotionEvent(String type, MotionEvent event) {
+ final int action = event.getAction();
+ final int N = event.getHistorySize();
+ final int NI = event.getPointerCount();
+ for (int historyPos = 0; historyPos < N; historyPos++) {
+ for (int i = 0; i < NI; i++) {
+ final int id = event.getPointerId(i);
+ event.getHistoricalPointerCoords(i, historyPos, mTempCoords);
+ logCoords(type, action, i, mTempCoords, id, event);
+ }
+ }
+ for (int i = 0; i < NI; i++) {
+ final int id = event.getPointerId(i);
+ event.getPointerCoords(i, mTempCoords);
+ logCoords(type, action, i, mTempCoords, id, event);
+ }
+ }
+
+ private void logCoords(String type, int action, int index,
+ MotionEvent.PointerCoords coords, int id, MotionEvent event) {
+ final int toolType = event.getToolType(index);
+ final int buttonState = event.getButtonState();
+ final String prefix;
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ prefix = "DOWN";
+ break;
+ case MotionEvent.ACTION_UP:
+ prefix = "UP";
+ break;
+ case MotionEvent.ACTION_MOVE:
+ prefix = "MOVE";
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ prefix = "CANCEL";
+ break;
+ case MotionEvent.ACTION_OUTSIDE:
+ prefix = "OUTSIDE";
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
+ prefix = "DOWN";
+ } else {
+ prefix = "MOVE";
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
+ prefix = "UP";
+ } else {
+ prefix = "MOVE";
+ }
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ prefix = "HOVER MOVE";
+ break;
+ case MotionEvent.ACTION_HOVER_ENTER:
+ prefix = "HOVER ENTER";
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ prefix = "HOVER EXIT";
+ break;
+ case MotionEvent.ACTION_SCROLL:
+ prefix = "SCROLL";
+ break;
+ default:
+ prefix = Integer.toString(action);
+ break;
+ }
+
+ Log.i(TAG, mText.clear()
+ .append(type).append(" id ").append(id + 1)
+ .append(": ")
+ .append(prefix)
+ .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3)
+ .append(") Pressure=").append(coords.pressure, 3)
+ .append(" Size=").append(coords.size, 3)
+ .append(" TouchMajor=").append(coords.touchMajor, 3)
+ .append(" TouchMinor=").append(coords.touchMinor, 3)
+ .append(" ToolMajor=").append(coords.toolMajor, 3)
+ .append(" ToolMinor=").append(coords.toolMinor, 3)
+ .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
+ .append("deg")
+ .append(" Tilt=").append((float)(
+ coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1)
+ .append("deg")
+ .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1)
+ .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1)
+ .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1)
+ .append(" BoundingBox=[(")
+ .append(event.getAxisValue(MotionEvent.AXIS_GENERIC_1), 3)
+ .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_2), 3).append(")")
+ .append(", (").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_3), 3)
+ .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_4), 3)
+ .append(")]")
+ .append(" ToolType=").append(MotionEvent.toolTypeToString(toolType))
+ .append(" ButtonState=").append(MotionEvent.buttonStateToString(buttonState))
+ .toString());
+ }
+
+ @Override
+ public void onPointerEvent(MotionEvent event) {
+ final int action = event.getAction();
+ int NP = mPointers.size();
+
+ if (action == MotionEvent.ACTION_DOWN
+ || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
+ final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
+ if (action == MotionEvent.ACTION_DOWN) {
+ for (int p=0; p<NP; p++) {
+ final PointerState ps = mPointers.get(p);
+ ps.clearTrace();
+ ps.mCurDown = false;
+ }
+ mCurDown = true;
+ mCurNumPointers = 0;
+ mMaxNumPointers = 0;
+ mVelocity.clear();
+ if (mAltVelocity != null) {
+ mAltVelocity.clear();
+ }
+ }
+
+ mCurNumPointers += 1;
+ if (mMaxNumPointers < mCurNumPointers) {
+ mMaxNumPointers = mCurNumPointers;
+ }
+
+ final int id = event.getPointerId(index);
+ while (NP <= id) {
+ PointerState ps = new PointerState();
+ mPointers.add(ps);
+ NP++;
+ }
+
+ if (mActivePointerId < 0 ||
+ !mPointers.get(mActivePointerId).mCurDown) {
+ mActivePointerId = id;
+ }
+
+ final PointerState ps = mPointers.get(id);
+ ps.mCurDown = true;
+ InputDevice device = InputDevice.getDevice(event.getDeviceId());
+ ps.mHasBoundingBox = device != null &&
+ device.getMotionRange(MotionEvent.AXIS_GENERIC_1) != null;
+ }
+
+ final int NI = event.getPointerCount();
+
+ mVelocity.addMovement(event);
+ mVelocity.computeCurrentVelocity(1);
+ if (mAltVelocity != null) {
+ mAltVelocity.addMovement(event);
+ mAltVelocity.computeCurrentVelocity(1);
+ }
+
+ final int N = event.getHistorySize();
+ for (int historyPos = 0; historyPos < N; historyPos++) {
+ for (int i = 0; i < NI; i++) {
+ final int id = event.getPointerId(i);
+ final PointerState ps = mCurDown ? mPointers.get(id) : null;
+ final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
+ event.getHistoricalPointerCoords(i, historyPos, coords);
+ if (mPrintCoords) {
+ logCoords("Pointer", action, i, coords, id, event);
+ }
+ if (ps != null) {
+ ps.addTrace(coords.x, coords.y, false);
+ }
+ }
+ }
+ for (int i = 0; i < NI; i++) {
+ final int id = event.getPointerId(i);
+ final PointerState ps = mCurDown ? mPointers.get(id) : null;
+ final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
+ event.getPointerCoords(i, coords);
+ if (mPrintCoords) {
+ logCoords("Pointer", action, i, coords, id, event);
+ }
+ if (ps != null) {
+ ps.addTrace(coords.x, coords.y, true);
+ ps.mXVelocity = mVelocity.getXVelocity(id);
+ ps.mYVelocity = mVelocity.getYVelocity(id);
+ mVelocity.getEstimator(id, ps.mEstimator);
+ if (mAltVelocity != null) {
+ ps.mAltXVelocity = mAltVelocity.getXVelocity(id);
+ ps.mAltYVelocity = mAltVelocity.getYVelocity(id);
+ mAltVelocity.getEstimator(id, ps.mAltEstimator);
+ }
+ ps.mToolType = event.getToolType(i);
+
+ if (ps.mHasBoundingBox) {
+ ps.mBoundingLeft = event.getAxisValue(MotionEvent.AXIS_GENERIC_1, i);
+ ps.mBoundingTop = event.getAxisValue(MotionEvent.AXIS_GENERIC_2, i);
+ ps.mBoundingRight = event.getAxisValue(MotionEvent.AXIS_GENERIC_3, i);
+ ps.mBoundingBottom = event.getAxisValue(MotionEvent.AXIS_GENERIC_4, i);
+ }
+ }
+ }
+
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
+ final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
+
+ final int id = event.getPointerId(index);
+ if (id >= NP) {
+ Slog.wtf(TAG, "Got pointer ID out of bounds: id=" + id + " arraysize="
+ + NP + " pointerindex=" + index
+ + " action=0x" + Integer.toHexString(action));
+ return;
+ }
+ final PointerState ps = mPointers.get(id);
+ ps.mCurDown = false;
+
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL) {
+ mCurDown = false;
+ mCurNumPointers = 0;
+ } else {
+ mCurNumPointers -= 1;
+ if (mActivePointerId == id) {
+ mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
+ }
+ ps.addTrace(Float.NaN, Float.NaN, false);
+ }
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ onPointerEvent(event);
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) {
+ requestFocus();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ final int source = event.getSource();
+ if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ onPointerEvent(event);
+ } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+ logMotionEvent("Joystick", event);
+ } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) {
+ logMotionEvent("Position", event);
+ } else {
+ logMotionEvent("Generic", event);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (shouldLogKey(keyCode)) {
+ final int repeatCount = event.getRepeatCount();
+ if (repeatCount == 0) {
+ Log.i(TAG, "Key Down: " + event);
+ } else {
+ Log.i(TAG, "Key Repeat #" + repeatCount + ": " + event);
+ }
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (shouldLogKey(keyCode)) {
+ Log.i(TAG, "Key Up: " + event);
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private static boolean shouldLogKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ return true;
+ default:
+ return KeyEvent.isGamepadButton(keyCode)
+ || KeyEvent.isModifierKey(keyCode);
+ }
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ logMotionEvent("Trackball", event);
+ return true;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mIm.registerInputDeviceListener(this, getHandler());
+ if (shouldShowSystemGestureExclusion()) {
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .registerSystemGestureExclusionListener(mSystemGestureExclusionListener,
+ mContext.getDisplayId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ final int alpha = systemGestureExclusionOpacity();
+ mSystemGestureExclusionPaint.setAlpha(alpha);
+ mSystemGestureExclusionRejectedPaint.setAlpha(alpha);
+ } else {
+ mSystemGestureExclusion.setEmpty();
+ }
+ logInputDevices();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ mIm.unregisterInputDeviceListener(this);
+ try {
+ WindowManagerGlobal.getWindowManagerService().unregisterSystemGestureExclusionListener(
+ mSystemGestureExclusionListener, mContext.getDisplayId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ logInputDeviceState(deviceId, "Device Added");
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ logInputDeviceState(deviceId, "Device Changed");
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ logInputDeviceState(deviceId, "Device Removed");
+ }
+
+ private void logInputDevices() {
+ int[] deviceIds = InputDevice.getDeviceIds();
+ for (int i = 0; i < deviceIds.length; i++) {
+ logInputDeviceState(deviceIds[i], "Device Enumerated");
+ }
+ }
+
+ private void logInputDeviceState(int deviceId, String state) {
+ InputDevice device = mIm.getInputDevice(deviceId);
+ if (device != null) {
+ Log.i(TAG, state + ": " + device);
+ } else {
+ Log.i(TAG, state + ": " + deviceId);
+ }
+ }
+
+ private static boolean shouldShowSystemGestureExclusion() {
+ return systemGestureExclusionOpacity() > 0;
+ }
+
+ private static int systemGestureExclusionOpacity() {
+ int x = SystemProperties.getInt(GESTURE_EXCLUSION_PROP, 0);
+ return x >= 0 && x <= 255 ? x : 0;
+ }
+
+ // HACK
+ // A quick and dirty string builder implementation optimized for GC.
+ // Using String.format causes the application grind to a halt when
+ // more than a couple of pointers are down due to the number of
+ // temporary objects allocated while formatting strings for drawing or logging.
+ private static final class FasterStringBuilder {
+ private char[] mChars;
+ private int mLength;
+
+ public FasterStringBuilder() {
+ mChars = new char[64];
+ }
+
+ public FasterStringBuilder clear() {
+ mLength = 0;
+ return this;
+ }
+
+ public FasterStringBuilder append(String value) {
+ final int valueLength = value.length();
+ final int index = reserve(valueLength);
+ value.getChars(0, valueLength, mChars, index);
+ mLength += valueLength;
+ return this;
+ }
+
+ public FasterStringBuilder append(int value) {
+ return append(value, 0);
+ }
+
+ public FasterStringBuilder append(int value, int zeroPadWidth) {
+ final boolean negative = value < 0;
+ if (negative) {
+ value = - value;
+ if (value < 0) {
+ append("-2147483648");
+ return this;
+ }
+ }
+
+ int index = reserve(11);
+ final char[] chars = mChars;
+
+ if (value == 0) {
+ chars[index++] = '0';
+ mLength += 1;
+ return this;
+ }
+
+ if (negative) {
+ chars[index++] = '-';
+ }
+
+ int divisor = 1000000000;
+ int numberWidth = 10;
+ while (value < divisor) {
+ divisor /= 10;
+ numberWidth -= 1;
+ if (numberWidth < zeroPadWidth) {
+ chars[index++] = '0';
+ }
+ }
+
+ do {
+ int digit = value / divisor;
+ value -= digit * divisor;
+ divisor /= 10;
+ chars[index++] = (char) (digit + '0');
+ } while (divisor != 0);
+
+ mLength = index;
+ return this;
+ }
+
+ public FasterStringBuilder append(float value, int precision) {
+ int scale = 1;
+ for (int i = 0; i < precision; i++) {
+ scale *= 10;
+ }
+ value = (float) (Math.rint(value * scale) / scale);
+
+ // Corner case: (int)-0.1 will become zero, so the negative sign gets lost
+ if ((int) value == 0 && value < 0) {
+ append("-");
+ }
+ append((int) value);
+
+ if (precision != 0) {
+ append(".");
+ value = Math.abs(value);
+ value -= Math.floor(value);
+ append((int) (value * scale), precision);
+ }
+
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return new String(mChars, 0, mLength);
+ }
+
+ private int reserve(int length) {
+ final int oldLength = mLength;
+ final int newLength = mLength + length;
+ final char[] oldChars = mChars;
+ final int oldCapacity = oldChars.length;
+ if (newLength > oldCapacity) {
+ final int newCapacity = oldCapacity * 2;
+ final char[] newChars = new char[newCapacity];
+ System.arraycopy(oldChars, 0, newChars, 0, oldLength);
+ mChars = newChars;
+ }
+ return oldLength;
+ }
+ }
+
+ private ISystemGestureExclusionListener mSystemGestureExclusionListener =
+ new ISystemGestureExclusionListener.Stub() {
+ @Override
+ public void onSystemGestureExclusionChanged(int displayId, Region systemGestureExclusion,
+ Region systemGestureExclusionUnrestricted) {
+ Region exclusion = Region.obtain(systemGestureExclusion);
+ Region rejected = Region.obtain();
+ if (systemGestureExclusionUnrestricted != null) {
+ rejected.set(systemGestureExclusionUnrestricted);
+ rejected.op(exclusion, Region.Op.DIFFERENCE);
+ }
+ Handler handler = getHandler();
+ if (handler != null) {
+ handler.post(() -> {
+ mSystemGestureExclusion.set(exclusion);
+ mSystemGestureExclusionRejected.set(rejected);
+ exclusion.recycle();
+ invalidate();
+ });
+ }
+ }
+ };
+}
diff --git a/com/android/internal/widget/PreferenceImageView.java b/com/android/internal/widget/PreferenceImageView.java
new file mode 100644
index 0000000..43b6b5a
--- /dev/null
+++ b/com/android/internal/widget/PreferenceImageView.java
@@ -0,0 +1,71 @@
+/*
+ * 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.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * Extension of ImageView that correctly applies maxWidth and maxHeight.
+ */
+public class PreferenceImageView extends ImageView {
+
+ public PreferenceImageView(Context context) {
+ this(context, null);
+ }
+
+ @UnsupportedAppUsage
+ public PreferenceImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int maxWidth = getMaxWidth();
+ if (maxWidth != Integer.MAX_VALUE
+ && (maxWidth < widthSize || widthMode == MeasureSpec.UNSPECIFIED)) {
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
+ }
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int maxHeight = getMaxHeight();
+ if (maxHeight != Integer.MAX_VALUE
+ && (maxHeight < heightSize || heightMode == MeasureSpec.UNSPECIFIED)) {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
+ }
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/com/android/internal/widget/RebootEscrowListener.java b/com/android/internal/widget/RebootEscrowListener.java
new file mode 100644
index 0000000..1654532
--- /dev/null
+++ b/com/android/internal/widget/RebootEscrowListener.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 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.internal.widget;
+
+/**
+ * Private API to be notified about reboot escrow events.
+ *
+ * {@hide}
+ */
+public interface RebootEscrowListener {
+ /**
+ * Called when the preparation status has changed. When {@code prepared} is {@code true} the
+ * user has entered their lock screen knowledge factor (LSKF) and the HAL has confirmed that
+ * it is ready to retrieve the secret after a reboot. When {@code prepared} is {@code false}
+ * then those conditions are not true.
+ */
+ void onPreparedForReboot(boolean prepared);
+}
diff --git a/com/android/internal/widget/RecyclerView.java b/com/android/internal/widget/RecyclerView.java
new file mode 100644
index 0000000..d7a01c4
--- /dev/null
+++ b/com/android/internal/widget/RecyclerView.java
@@ -0,0 +1,12264 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.Observable;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.AbsSavedState;
+import android.view.Display;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+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.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
+import android.widget.EdgeEffect;
+import android.widget.OverScroller;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A flexible view for providing a limited window into a large data set.
+ *
+ * <h3>Glossary of terms:</h3>
+ *
+ * <ul>
+ * <li><em>Adapter:</em> A subclass of {@link Adapter} responsible for providing views
+ * that represent items in a data set.</li>
+ * <li><em>Position:</em> The position of a data item within an <em>Adapter</em>.</li>
+ * <li><em>Index:</em> The index of an attached child view as used in a call to
+ * {@link ViewGroup#getChildAt}. Contrast with <em>Position.</em></li>
+ * <li><em>Binding:</em> The process of preparing a child view to display data corresponding
+ * to a <em>position</em> within the adapter.</li>
+ * <li><em>Recycle (view):</em> A view previously used to display data for a specific adapter
+ * position may be placed in a cache for later reuse to display the same type of data again
+ * later. This can drastically improve performance by skipping initial layout inflation
+ * or construction.</li>
+ * <li><em>Scrap (view):</em> A child view that has entered into a temporarily detached
+ * state during layout. Scrap views may be reused without becoming fully detached
+ * from the parent RecyclerView, either unmodified if no rebinding is required or modified
+ * by the adapter if the view was considered <em>dirty</em>.</li>
+ * <li><em>Dirty (view):</em> A child view that must be rebound by the adapter before
+ * being displayed.</li>
+ * </ul>
+ *
+ * <h4>Positions in RecyclerView:</h4>
+ * <p>
+ * RecyclerView introduces an additional level of abstraction between the {@link Adapter} and
+ * {@link LayoutManager} to be able to detect data set changes in batches during a layout
+ * calculation. This saves LayoutManager from tracking adapter changes to calculate animations.
+ * It also helps with performance because all view bindings happen at the same time and unnecessary
+ * bindings are avoided.
+ * <p>
+ * For this reason, there are two types of <code>position</code> related methods in RecyclerView:
+ * <ul>
+ * <li>layout position: Position of an item in the latest layout calculation. This is the
+ * position from the LayoutManager's perspective.</li>
+ * <li>adapter position: Position of an item in the adapter. This is the position from
+ * the Adapter's perspective.</li>
+ * </ul>
+ * <p>
+ * These two positions are the same except the time between dispatching <code>adapter.notify*
+ * </code> events and calculating the updated layout.
+ * <p>
+ * Methods that return or receive <code>*LayoutPosition*</code> use position as of the latest
+ * layout calculation (e.g. {@link ViewHolder#getLayoutPosition()},
+ * {@link #findViewHolderForLayoutPosition(int)}). These positions include all changes until the
+ * last layout calculation. You can rely on these positions to be consistent with what user is
+ * currently seeing on the screen. For example, if you have a list of items on the screen and user
+ * asks for the 5<sup>th</sup> element, you should use these methods as they'll match what user
+ * is seeing.
+ * <p>
+ * The other set of position related methods are in the form of
+ * <code>*AdapterPosition*</code>. (e.g. {@link ViewHolder#getAdapterPosition()},
+ * {@link #findViewHolderForAdapterPosition(int)}) You should use these methods when you need to
+ * work with up-to-date adapter positions even if they may not have been reflected to layout yet.
+ * For example, if you want to access the item in the adapter on a ViewHolder click, you should use
+ * {@link ViewHolder#getAdapterPosition()}. Beware that these methods may not be able to calculate
+ * adapter positions if {@link Adapter#notifyDataSetChanged()} has been called and new layout has
+ * not yet been calculated. For this reasons, you should carefully handle {@link #NO_POSITION} or
+ * <code>null</code> results from these methods.
+ * <p>
+ * When writing a {@link LayoutManager} you almost always want to use layout positions whereas when
+ * writing an {@link Adapter}, you probably want to use adapter positions.
+ */
+public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
+
+ static final String TAG = "RecyclerView";
+
+ static final boolean DEBUG = false;
+
+ private static final int[] NESTED_SCROLLING_ATTRS = { android.R.attr.nestedScrollingEnabled };
+
+ private static final int[] CLIP_TO_PADDING_ATTR = {android.R.attr.clipToPadding};
+
+ /**
+ * On Kitkat and JB MR2, there is a bug which prevents DisplayList from being invalidated if
+ * a View is two levels deep(wrt to ViewHolder.itemView). DisplayList can be invalidated by
+ * setting View's visibility to INVISIBLE when View is detached. On Kitkat and JB MR2, Recycler
+ * recursively traverses itemView and invalidates display list for each ViewGroup that matches
+ * this criteria.
+ */
+ static final boolean FORCE_INVALIDATE_DISPLAY_LIST = Build.VERSION.SDK_INT == 18
+ || Build.VERSION.SDK_INT == 19 || Build.VERSION.SDK_INT == 20;
+ /**
+ * On M+, an unspecified measure spec may include a hint which we can use. On older platforms,
+ * this value might be garbage. To save LayoutManagers from it, RecyclerView sets the size to
+ * 0 when mode is unspecified.
+ */
+ static final boolean ALLOW_SIZE_IN_UNSPECIFIED_SPEC = Build.VERSION.SDK_INT >= 23;
+
+ static final boolean POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16;
+
+ /**
+ * On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to
+ * RenderThread but before the next frame begins. We schedule prefetch work in this window.
+ */
+ private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
+
+ /**
+ * FocusFinder#findNextFocus is broken on ICS MR1 and older for View.FOCUS_BACKWARD direction.
+ * We convert it to an absolute direction such as FOCUS_DOWN or FOCUS_LEFT.
+ */
+ private static final boolean FORCE_ABS_FOCUS_SEARCH_DIRECTION = Build.VERSION.SDK_INT <= 15;
+
+ /**
+ * on API 15-, a focused child can still be considered a focused child of RV even after
+ * it's being removed or its focusable flag is set to false. This is because when this focused
+ * child is detached, the reference to this child is not removed in clearFocus. API 16 and above
+ * properly handle this case by calling ensureInputFocusOnFirstFocusable or rootViewRequestFocus
+ * to request focus on a new child, which will clear the focus on the old (detached) child as a
+ * side-effect.
+ */
+ private static final boolean IGNORE_DETACHED_FOCUSED_CHILD = Build.VERSION.SDK_INT <= 15;
+
+ static final boolean DISPATCH_TEMP_DETACH = false;
+ public static final int HORIZONTAL = 0;
+ public static final int VERTICAL = 1;
+
+ public static final int NO_POSITION = -1;
+ public static final long NO_ID = -1;
+ public static final int INVALID_TYPE = -1;
+
+ /**
+ * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates
+ * that the RecyclerView should use the standard touch slop for smooth,
+ * continuous scrolling.
+ */
+ public static final int TOUCH_SLOP_DEFAULT = 0;
+
+ /**
+ * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates
+ * that the RecyclerView should use the standard touch slop for scrolling
+ * widgets that snap to a page or other coarse-grained barrier.
+ */
+ public static final int TOUCH_SLOP_PAGING = 1;
+
+ static final int MAX_SCROLL_DURATION = 2000;
+
+ /**
+ * RecyclerView is calculating a scroll.
+ * If there are too many of these in Systrace, some Views inside RecyclerView might be causing
+ * it. Try to avoid using EditText, focusable views or handle them with care.
+ */
+ static final String TRACE_SCROLL_TAG = "RV Scroll";
+
+ /**
+ * OnLayout has been called by the View system.
+ * If this shows up too many times in Systrace, make sure the children of RecyclerView do not
+ * update themselves directly. This will cause a full re-layout but when it happens via the
+ * Adapter notifyItemChanged, RecyclerView can avoid full layout calculation.
+ */
+ private static final String TRACE_ON_LAYOUT_TAG = "RV OnLayout";
+
+ /**
+ * NotifyDataSetChanged or equal has been called.
+ * If this is taking a long time, try sending granular notify adapter changes instead of just
+ * calling notifyDataSetChanged or setAdapter / swapAdapter. Adding stable ids to your adapter
+ * might help.
+ */
+ private static final String TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG = "RV FullInvalidate";
+
+ /**
+ * RecyclerView is doing a layout for partial adapter updates (we know what has changed)
+ * If this is taking a long time, you may have dispatched too many Adapter updates causing too
+ * many Views being rebind. Make sure all are necessary and also prefer using notify*Range
+ * methods.
+ */
+ private static final String TRACE_HANDLE_ADAPTER_UPDATES_TAG = "RV PartialInvalidate";
+
+ /**
+ * RecyclerView is rebinding a View.
+ * If this is taking a lot of time, consider optimizing your layout or make sure you are not
+ * doing extra operations in onBindViewHolder call.
+ */
+ static final String TRACE_BIND_VIEW_TAG = "RV OnBindView";
+
+ /**
+ * RecyclerView is attempting to pre-populate off screen views.
+ */
+ static final String TRACE_PREFETCH_TAG = "RV Prefetch";
+
+ /**
+ * RecyclerView is attempting to pre-populate off screen itemviews within an off screen
+ * RecyclerView.
+ */
+ static final String TRACE_NESTED_PREFETCH_TAG = "RV Nested Prefetch";
+
+ /**
+ * RecyclerView is creating a new View.
+ * If too many of these present in Systrace:
+ * - There might be a problem in Recycling (e.g. custom Animations that set transient state and
+ * prevent recycling or ItemAnimator not implementing the contract properly. ({@link
+ * > Adapter#onFailedToRecycleView(ViewHolder)})
+ *
+ * - There might be too many item view types.
+ * > Try merging them
+ *
+ * - There might be too many itemChange animations and not enough space in RecyclerPool.
+ * >Try increasing your pool size and item cache size.
+ */
+ static final String TRACE_CREATE_VIEW_TAG = "RV CreateView";
+ private static final Class<?>[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE =
+ new Class[]{Context.class, AttributeSet.class, int.class, int.class};
+
+ private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
+
+ final Recycler mRecycler = new Recycler();
+
+ private SavedState mPendingSavedState;
+
+ /**
+ * Handles adapter updates
+ */
+ AdapterHelper mAdapterHelper;
+
+ /**
+ * Handles abstraction between LayoutManager children and RecyclerView children
+ */
+ ChildHelper mChildHelper;
+
+ /**
+ * Keeps data about views to be used for animations
+ */
+ final ViewInfoStore mViewInfoStore = new ViewInfoStore();
+
+ /**
+ * Prior to L, there is no way to query this variable which is why we override the setter and
+ * track it here.
+ */
+ boolean mClipToPadding;
+
+ /**
+ * Note: this Runnable is only ever posted if:
+ * 1) We've been through first layout
+ * 2) We know we have a fixed size (mHasFixedSize)
+ * 3) We're attached
+ */
+ final Runnable mUpdateChildViewsRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!mFirstLayoutComplete || isLayoutRequested()) {
+ // a layout request will happen, we should not do layout here.
+ return;
+ }
+ if (!mIsAttached) {
+ requestLayout();
+ // if we are not attached yet, mark us as requiring layout and skip
+ return;
+ }
+ if (mLayoutFrozen) {
+ mLayoutRequestEaten = true;
+ return; //we'll process updates when ice age ends.
+ }
+ consumePendingUpdateOperations();
+ }
+ };
+
+ final Rect mTempRect = new Rect();
+ private final Rect mTempRect2 = new Rect();
+ final RectF mTempRectF = new RectF();
+ Adapter mAdapter;
+ @VisibleForTesting LayoutManager mLayout;
+ RecyclerListener mRecyclerListener;
+ final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
+ private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
+ new ArrayList<>();
+ private OnItemTouchListener mActiveOnItemTouchListener;
+ boolean mIsAttached;
+ boolean mHasFixedSize;
+ @VisibleForTesting boolean mFirstLayoutComplete;
+
+ // Counting lock to control whether we should ignore requestLayout calls from children or not.
+ private int mEatRequestLayout = 0;
+
+ boolean mLayoutRequestEaten;
+ boolean mLayoutFrozen;
+ private boolean mIgnoreMotionEventTillDown;
+
+ // binary OR of change events that were eaten during a layout or scroll.
+ private int mEatenAccessibilityChangeFlags;
+ boolean mAdapterUpdateDuringMeasure;
+
+ private final AccessibilityManager mAccessibilityManager;
+ private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners;
+
+ /**
+ * Set to true when an adapter data set changed notification is received.
+ * In that case, we cannot run any animations since we don't know what happened until layout.
+ *
+ * Attached items are invalid until next layout, at which point layout will animate/replace
+ * items as necessary, building up content from the (effectively) new adapter from scratch.
+ *
+ * Cached items must be discarded when setting this to true, so that the cache may be freely
+ * used by prefetching until the next layout occurs.
+ *
+ * @see #setDataSetChangedAfterLayout()
+ */
+ boolean mDataSetHasChangedAfterLayout = false;
+
+ /**
+ * This variable is incremented during a dispatchLayout and/or scroll.
+ * Some methods should not be called during these periods (e.g. adapter data change).
+ * Doing so will create hard to find bugs so we better check it and throw an exception.
+ *
+ * @see #assertInLayoutOrScroll(String)
+ * @see #assertNotInLayoutOrScroll(String)
+ */
+ private int mLayoutOrScrollCounter = 0;
+
+ /**
+ * Similar to mLayoutOrScrollCounter but logs a warning instead of throwing an exception
+ * (for API compatibility).
+ * <p>
+ * It is a bad practice for a developer to update the data in a scroll callback since it is
+ * potentially called during a layout.
+ */
+ private int mDispatchScrollCounter = 0;
+
+ private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
+
+ ItemAnimator mItemAnimator = new DefaultItemAnimator();
+
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * The RecyclerView is not currently scrolling.
+ * @see #getScrollState()
+ */
+ public static final int SCROLL_STATE_IDLE = 0;
+
+ /**
+ * The RecyclerView is currently being dragged by outside input such as user touch input.
+ * @see #getScrollState()
+ */
+ public static final int SCROLL_STATE_DRAGGING = 1;
+
+ /**
+ * The RecyclerView is currently animating to a final position while not under
+ * outside control.
+ * @see #getScrollState()
+ */
+ public static final int SCROLL_STATE_SETTLING = 2;
+
+ static final long FOREVER_NS = Long.MAX_VALUE;
+
+ // Touch/scrolling handling
+
+ private int mScrollState = SCROLL_STATE_IDLE;
+ private int mScrollPointerId = INVALID_POINTER;
+ private VelocityTracker mVelocityTracker;
+ private int mInitialTouchX;
+ private int mInitialTouchY;
+ private int mLastTouchX;
+ private int mLastTouchY;
+ private int mTouchSlop;
+ private OnFlingListener mOnFlingListener;
+ private final int mMinFlingVelocity;
+ private final int mMaxFlingVelocity;
+ // This value is used when handling generic motion events.
+ private float mScrollFactor = Float.MIN_VALUE;
+ private boolean mPreserveFocusAfterLayout = true;
+
+ final ViewFlinger mViewFlinger = new ViewFlinger();
+
+ GapWorker mGapWorker;
+ GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry =
+ ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null;
+
+ final State mState = new State();
+
+ private OnScrollListener mScrollListener;
+ private List<OnScrollListener> mScrollListeners;
+
+ // For use in item animations
+ boolean mItemsAddedOrRemoved = false;
+ boolean mItemsChanged = false;
+ private ItemAnimator.ItemAnimatorListener mItemAnimatorListener =
+ new ItemAnimatorRestoreListener();
+ boolean mPostedAnimatorRunner = false;
+ RecyclerViewAccessibilityDelegate mAccessibilityDelegate;
+ private ChildDrawingOrderCallback mChildDrawingOrderCallback;
+
+ // simple array to keep min and max child position during a layout calculation
+ // preserved not to create a new one in each layout pass
+ private final int[] mMinMaxLayoutPositions = new int[2];
+
+ private final int[] mScrollOffset = new int[2];
+ private final int[] mScrollConsumed = new int[2];
+ private final int[] mNestedOffsets = new int[2];
+
+ /**
+ * These are views that had their a11y importance changed during a layout. We defer these events
+ * until the end of the layout because a11y service may make sync calls back to the RV while
+ * the View's state is undefined.
+ */
+ @VisibleForTesting
+ final List<ViewHolder> mPendingAccessibilityImportanceChange = new ArrayList();
+
+ private Runnable mItemAnimatorRunner = new Runnable() {
+ @Override
+ public void run() {
+ if (mItemAnimator != null) {
+ mItemAnimator.runPendingAnimations();
+ }
+ mPostedAnimatorRunner = false;
+ }
+ };
+
+ static final Interpolator sQuinticInterpolator = new Interpolator() {
+ @Override
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ /**
+ * The callback to convert view info diffs into animations.
+ */
+ private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
+ new ViewInfoStore.ProcessCallback() {
+ @Override
+ public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
+ @Nullable ItemHolderInfo postInfo) {
+ mRecycler.unscrapView(viewHolder);
+ animateDisappearance(viewHolder, info, postInfo);
+ }
+ @Override
+ public void processAppeared(ViewHolder viewHolder,
+ ItemHolderInfo preInfo, ItemHolderInfo info) {
+ animateAppearance(viewHolder, preInfo, info);
+ }
+
+ @Override
+ public void processPersistent(ViewHolder viewHolder,
+ @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
+ viewHolder.setIsRecyclable(false);
+ if (mDataSetHasChangedAfterLayout) {
+ // since it was rebound, use change instead as we'll be mapping them from
+ // stable ids. If stable ids were false, we would not be running any
+ // animations
+ if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {
+ postAnimationRunner();
+ }
+ } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
+ postAnimationRunner();
+ }
+ }
+ @Override
+ public void unused(ViewHolder viewHolder) {
+ mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
+ }
+ };
+
+ public RecyclerView(Context context) {
+ this(context, null);
+ }
+
+ public RecyclerView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ if (attrs != null) {
+ TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0);
+ mClipToPadding = a.getBoolean(0, true);
+ a.recycle();
+ } else {
+ mClipToPadding = true;
+ }
+ setScrollContainer(true);
+ setFocusableInTouchMode(true);
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+ setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
+
+ mItemAnimator.setListener(mItemAnimatorListener);
+ initAdapterManager();
+ initChildrenHelper();
+ // If not explicitly specified this view is important for accessibility.
+ if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ mAccessibilityManager = (AccessibilityManager) getContext()
+ .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this));
+ // Create the layoutManager if specified.
+
+ boolean nestedScrollingEnabled = true;
+
+ if (attrs != null) {
+ int defStyleRes = 0;
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
+ defStyle, defStyleRes);
+ String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
+ int descendantFocusability = a.getInt(
+ R.styleable.RecyclerView_descendantFocusability, -1);
+ if (descendantFocusability == -1) {
+ setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ }
+ a.recycle();
+ createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
+ defStyle, defStyleRes);
+ nestedScrollingEnabled = a.getBoolean(0, true);
+ a.recycle();
+ }
+ } else {
+ setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ }
+
+ // Re-set whether nested scrolling is enabled so that it is set on all API levels
+ setNestedScrollingEnabled(nestedScrollingEnabled);
+ }
+
+ /**
+ * Returns the accessibility delegate compatibility implementation used by the RecyclerView.
+ * @return An instance of AccessibilityDelegateCompat used by RecyclerView
+ */
+ public RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate() {
+ return mAccessibilityDelegate;
+ }
+
+ /**
+ * Sets the accessibility delegate compatibility implementation used by RecyclerView.
+ * @param accessibilityDelegate The accessibility delegate to be used by RecyclerView.
+ */
+ public void setAccessibilityDelegateCompat(
+ RecyclerViewAccessibilityDelegate accessibilityDelegate) {
+ mAccessibilityDelegate = accessibilityDelegate;
+ setAccessibilityDelegate(mAccessibilityDelegate);
+ }
+
+ /**
+ * Instantiate and set a LayoutManager, if specified in the attributes.
+ */
+ private void createLayoutManager(Context context, String className, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ if (className != null) {
+ className = className.trim();
+ if (className.length() != 0) { // Can't use isEmpty since it was added in API 9.
+ className = getFullClassName(context, className);
+ try {
+ ClassLoader classLoader;
+ if (isInEditMode()) {
+ // Stupid layoutlib cannot handle simple class loaders.
+ classLoader = this.getClass().getClassLoader();
+ } else {
+ classLoader = context.getClassLoader();
+ }
+ Class<? extends LayoutManager> layoutManagerClass =
+ classLoader.loadClass(className).asSubclass(LayoutManager.class);
+ Constructor<? extends LayoutManager> constructor;
+ Object[] constructorArgs = null;
+ try {
+ constructor = layoutManagerClass
+ .getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE);
+ constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes};
+ } catch (NoSuchMethodException e) {
+ try {
+ constructor = layoutManagerClass.getConstructor();
+ } catch (NoSuchMethodException e1) {
+ e1.initCause(e);
+ throw new IllegalStateException(attrs.getPositionDescription()
+ + ": Error creating LayoutManager " + className, e1);
+ }
+ }
+ constructor.setAccessible(true);
+ setLayoutManager(constructor.newInstance(constructorArgs));
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(attrs.getPositionDescription()
+ + ": Unable to find LayoutManager " + className, e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalStateException(attrs.getPositionDescription()
+ + ": Could not instantiate the LayoutManager: " + className, e);
+ } catch (InstantiationException e) {
+ throw new IllegalStateException(attrs.getPositionDescription()
+ + ": Could not instantiate the LayoutManager: " + className, e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(attrs.getPositionDescription()
+ + ": Cannot access non-public constructor " + className, e);
+ } catch (ClassCastException e) {
+ throw new IllegalStateException(attrs.getPositionDescription()
+ + ": Class is not a LayoutManager " + className, e);
+ }
+ }
+ }
+ }
+
+ private String getFullClassName(Context context, String className) {
+ if (className.charAt(0) == '.') {
+ return context.getPackageName() + className;
+ }
+ if (className.contains(".")) {
+ return className;
+ }
+ return RecyclerView.class.getPackage().getName() + '.' + className;
+ }
+
+ private void initChildrenHelper() {
+ mChildHelper = new ChildHelper(new ChildHelper.Callback() {
+ @Override
+ public int getChildCount() {
+ return RecyclerView.this.getChildCount();
+ }
+
+ @Override
+ public void addView(View child, int index) {
+ RecyclerView.this.addView(child, index);
+ dispatchChildAttached(child);
+ }
+
+ @Override
+ public int indexOfChild(View view) {
+ return RecyclerView.this.indexOfChild(view);
+ }
+
+ @Override
+ public void removeViewAt(int index) {
+ final View child = RecyclerView.this.getChildAt(index);
+ if (child != null) {
+ dispatchChildDetached(child);
+ }
+ RecyclerView.this.removeViewAt(index);
+ }
+
+ @Override
+ public View getChildAt(int offset) {
+ return RecyclerView.this.getChildAt(offset);
+ }
+
+ @Override
+ public void removeAllViews() {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ dispatchChildDetached(getChildAt(i));
+ }
+ RecyclerView.this.removeAllViews();
+ }
+
+ @Override
+ public ViewHolder getChildViewHolder(View view) {
+ return getChildViewHolderInt(view);
+ }
+
+ @Override
+ public void attachViewToParent(View child, int index,
+ ViewGroup.LayoutParams layoutParams) {
+ final ViewHolder vh = getChildViewHolderInt(child);
+ if (vh != null) {
+ if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
+ throw new IllegalArgumentException("Called attach on a child which is not"
+ + " detached: " + vh);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "reAttach " + vh);
+ }
+ vh.clearTmpDetachFlag();
+ }
+ RecyclerView.this.attachViewToParent(child, index, layoutParams);
+ }
+
+ @Override
+ public void detachViewFromParent(int offset) {
+ final View view = getChildAt(offset);
+ if (view != null) {
+ final ViewHolder vh = getChildViewHolderInt(view);
+ if (vh != null) {
+ if (vh.isTmpDetached() && !vh.shouldIgnore()) {
+ throw new IllegalArgumentException("called detach on an already"
+ + " detached child " + vh);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "tmpDetach " + vh);
+ }
+ vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
+ }
+ }
+ RecyclerView.this.detachViewFromParent(offset);
+ }
+
+ @Override
+ public void onEnteredHiddenState(View child) {
+ final ViewHolder vh = getChildViewHolderInt(child);
+ if (vh != null) {
+ vh.onEnteredHiddenState(RecyclerView.this);
+ }
+ }
+
+ @Override
+ public void onLeftHiddenState(View child) {
+ final ViewHolder vh = getChildViewHolderInt(child);
+ if (vh != null) {
+ vh.onLeftHiddenState(RecyclerView.this);
+ }
+ }
+ });
+ }
+
+ void initAdapterManager() {
+ mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
+ @Override
+ public ViewHolder findViewHolder(int position) {
+ final ViewHolder vh = findViewHolderForPosition(position, true);
+ if (vh == null) {
+ return null;
+ }
+ // ensure it is not hidden because for adapter helper, the only thing matter is that
+ // LM thinks view is a child.
+ if (mChildHelper.isHidden(vh.itemView)) {
+ if (DEBUG) {
+ Log.d(TAG, "assuming view holder cannot be find because it is hidden");
+ }
+ return null;
+ }
+ return vh;
+ }
+
+ @Override
+ public void offsetPositionsForRemovingInvisible(int start, int count) {
+ offsetPositionRecordsForRemove(start, count, true);
+ mItemsAddedOrRemoved = true;
+ mState.mDeletedInvisibleItemCountSincePreviousLayout += count;
+ }
+
+ @Override
+ public void offsetPositionsForRemovingLaidOutOrNewView(
+ int positionStart, int itemCount) {
+ offsetPositionRecordsForRemove(positionStart, itemCount, false);
+ mItemsAddedOrRemoved = true;
+ }
+
+ @Override
+ public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
+ viewRangeUpdate(positionStart, itemCount, payload);
+ mItemsChanged = true;
+ }
+
+ @Override
+ public void onDispatchFirstPass(AdapterHelper.UpdateOp op) {
+ dispatchUpdate(op);
+ }
+
+ void dispatchUpdate(AdapterHelper.UpdateOp op) {
+ switch (op.cmd) {
+ case AdapterHelper.UpdateOp.ADD:
+ mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount);
+ break;
+ case AdapterHelper.UpdateOp.REMOVE:
+ mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
+ break;
+ case AdapterHelper.UpdateOp.UPDATE:
+ mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount,
+ op.payload);
+ break;
+ case AdapterHelper.UpdateOp.MOVE:
+ mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
+ break;
+ }
+ }
+
+ @Override
+ public void onDispatchSecondPass(AdapterHelper.UpdateOp op) {
+ dispatchUpdate(op);
+ }
+
+ @Override
+ public void offsetPositionsForAdd(int positionStart, int itemCount) {
+ offsetPositionRecordsForInsert(positionStart, itemCount);
+ mItemsAddedOrRemoved = true;
+ }
+
+ @Override
+ public void offsetPositionsForMove(int from, int to) {
+ offsetPositionRecordsForMove(from, to);
+ // should we create mItemsMoved ?
+ mItemsAddedOrRemoved = true;
+ }
+ });
+ }
+
+ /**
+ * RecyclerView can perform several optimizations if it can know in advance that RecyclerView's
+ * size is not affected by the adapter contents. RecyclerView can still change its size based
+ * on other factors (e.g. its parent's size) but this size calculation cannot depend on the
+ * size of its children or contents of its adapter (except the number of items in the adapter).
+ * <p>
+ * If your use of RecyclerView falls into this category, set this to {@code true}. It will allow
+ * RecyclerView to avoid invalidating the whole layout when its adapter contents change.
+ *
+ * @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.
+ */
+ public void setHasFixedSize(boolean hasFixedSize) {
+ mHasFixedSize = hasFixedSize;
+ }
+
+ /**
+ * @return true if the app has specified that changes in adapter content cannot change
+ * the size of the RecyclerView itself.
+ */
+ public boolean hasFixedSize() {
+ return mHasFixedSize;
+ }
+
+ @Override
+ public void setClipToPadding(boolean clipToPadding) {
+ if (clipToPadding != mClipToPadding) {
+ invalidateGlows();
+ }
+ mClipToPadding = clipToPadding;
+ super.setClipToPadding(clipToPadding);
+ if (mFirstLayoutComplete) {
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns whether this RecyclerView will clip its children to its padding, and resize (but
+ * not clip) any EdgeEffect to the padded region, if padding is present.
+ * <p>
+ * By default, children are clipped to the padding of their parent
+ * RecyclerView. This clipping behavior is only enabled if padding is non-zero.
+ *
+ * @return true if this RecyclerView clips children to its padding and resizes (but doesn't
+ * clip) any EdgeEffect to the padded region, false otherwise.
+ *
+ * @attr name android:clipToPadding
+ */
+ @Override
+ public boolean getClipToPadding() {
+ return mClipToPadding;
+ }
+
+ /**
+ * Configure the scrolling touch slop for a specific use case.
+ *
+ * Set up the RecyclerView's scrolling motion threshold based on common usages.
+ * Valid arguments are {@link #TOUCH_SLOP_DEFAULT} and {@link #TOUCH_SLOP_PAGING}.
+ *
+ * @param slopConstant One of the <code>TOUCH_SLOP_</code> constants representing
+ * the intended usage of this RecyclerView
+ */
+ public void setScrollingTouchSlop(int slopConstant) {
+ final ViewConfiguration vc = ViewConfiguration.get(getContext());
+ switch (slopConstant) {
+ default:
+ Log.w(TAG, "setScrollingTouchSlop(): bad argument constant "
+ + slopConstant + "; using default value");
+ // fall-through
+ case TOUCH_SLOP_DEFAULT:
+ mTouchSlop = vc.getScaledTouchSlop();
+ break;
+
+ case TOUCH_SLOP_PAGING:
+ mTouchSlop = vc.getScaledPagingTouchSlop();
+ break;
+ }
+ }
+
+ /**
+ * Swaps the current adapter with the provided one. It is similar to
+ * {@link #setAdapter(Adapter)} but assumes existing adapter and the new adapter uses the same
+ * {@link ViewHolder} and does not clear the RecycledViewPool.
+ * <p>
+ * Note that it still calls onAdapterChanged callbacks.
+ *
+ * @param adapter The new adapter to set, or null to set no adapter.
+ * @param removeAndRecycleExistingViews If set to true, RecyclerView will recycle all existing
+ * Views. If adapters have stable ids and/or you want to
+ * animate the disappearing views, you may prefer to set
+ * this to false.
+ * @see #setAdapter(Adapter)
+ */
+ public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) {
+ // bail out if layout is frozen
+ setLayoutFrozen(false);
+ setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
+ setDataSetChangedAfterLayout();
+ requestLayout();
+ }
+ /**
+ * Set a new adapter to provide child views on demand.
+ * <p>
+ * When adapter is changed, all existing views are recycled back to the pool. If the pool has
+ * only one adapter, it will be cleared.
+ *
+ * @param adapter The new adapter to set, or null to set no adapter.
+ * @see #swapAdapter(Adapter, boolean)
+ */
+ public void setAdapter(Adapter adapter) {
+ // bail out if layout is frozen
+ setLayoutFrozen(false);
+ setAdapterInternal(adapter, false, true);
+ requestLayout();
+ }
+
+ /**
+ * Removes and recycles all views - both those currently attached, and those in the Recycler.
+ */
+ void removeAndRecycleViews() {
+ // end all running animations
+ if (mItemAnimator != null) {
+ mItemAnimator.endAnimations();
+ }
+ // Since animations are ended, mLayout.children should be equal to
+ // recyclerView.children. This may not be true if item animator's end does not work as
+ // expected. (e.g. not release children instantly). It is safer to use mLayout's child
+ // count.
+ if (mLayout != null) {
+ mLayout.removeAndRecycleAllViews(mRecycler);
+ mLayout.removeAndRecycleScrapInt(mRecycler);
+ }
+ // we should clear it here before adapters are swapped to ensure correct callbacks.
+ mRecycler.clear();
+ }
+
+ /**
+ * Replaces the current adapter with the new one and triggers listeners.
+ * @param adapter The new adapter
+ * @param compatibleWithPrevious If true, the new adapter is using the same View Holders and
+ * item types with the current adapter (helps us avoid cache
+ * invalidation).
+ * @param removeAndRecycleViews If true, we'll remove and recycle all existing views. If
+ * compatibleWithPrevious is false, this parameter is ignored.
+ */
+ private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
+ boolean removeAndRecycleViews) {
+ if (mAdapter != null) {
+ mAdapter.unregisterAdapterDataObserver(mObserver);
+ mAdapter.onDetachedFromRecyclerView(this);
+ }
+ if (!compatibleWithPrevious || removeAndRecycleViews) {
+ removeAndRecycleViews();
+ }
+ mAdapterHelper.reset();
+ final Adapter oldAdapter = mAdapter;
+ mAdapter = adapter;
+ if (adapter != null) {
+ adapter.registerAdapterDataObserver(mObserver);
+ adapter.onAttachedToRecyclerView(this);
+ }
+ if (mLayout != null) {
+ mLayout.onAdapterChanged(oldAdapter, mAdapter);
+ }
+ mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
+ mState.mStructureChanged = true;
+ markKnownViewsInvalid();
+ }
+
+ /**
+ * Retrieves the previously set adapter or null if no adapter is set.
+ *
+ * @return The previously set adapter
+ * @see #setAdapter(Adapter)
+ */
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Register a listener that will be notified whenever a child view is recycled.
+ *
+ * <p>This listener will be called when a LayoutManager or the RecyclerView decides
+ * that a child view is no longer needed. If an application associates expensive
+ * or heavyweight data with item views, this may be a good place to release
+ * or free those resources.</p>
+ *
+ * @param listener Listener to register, or null to clear
+ */
+ public void setRecyclerListener(RecyclerListener listener) {
+ mRecyclerListener = listener;
+ }
+
+ /**
+ * <p>Return the offset of the RecyclerView's text baseline from the its top
+ * boundary. If the LayoutManager of this RecyclerView does not support baseline alignment,
+ * this method returns -1.</p>
+ *
+ * @return the offset of the baseline within the RecyclerView's bounds or -1
+ * if baseline alignment is not supported
+ */
+ @Override
+ public int getBaseline() {
+ if (mLayout != null) {
+ return mLayout.getBaseline();
+ } else {
+ return super.getBaseline();
+ }
+ }
+
+ /**
+ * Register a listener that will be notified whenever a child view is attached to or detached
+ * from RecyclerView.
+ *
+ * <p>This listener will be called when a LayoutManager or the RecyclerView decides
+ * that a child view is no longer needed. If an application associates expensive
+ * or heavyweight data with item views, this may be a good place to release
+ * or free those resources.</p>
+ *
+ * @param listener Listener to register
+ */
+ public void addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) {
+ if (mOnChildAttachStateListeners == null) {
+ mOnChildAttachStateListeners = new ArrayList<>();
+ }
+ mOnChildAttachStateListeners.add(listener);
+ }
+
+ /**
+ * Removes the provided listener from child attached state listeners list.
+ *
+ * @param listener Listener to unregister
+ */
+ public void removeOnChildAttachStateChangeListener(OnChildAttachStateChangeListener listener) {
+ if (mOnChildAttachStateListeners == null) {
+ return;
+ }
+ mOnChildAttachStateListeners.remove(listener);
+ }
+
+ /**
+ * Removes all listeners that were added via
+ * {@link #addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener)}.
+ */
+ public void clearOnChildAttachStateChangeListeners() {
+ if (mOnChildAttachStateListeners != null) {
+ mOnChildAttachStateListeners.clear();
+ }
+ }
+
+ /**
+ * Set the {@link LayoutManager} that this RecyclerView will use.
+ *
+ * <p>In contrast to other adapter-backed views such as {@link android.widget.ListView}
+ * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom
+ * layout arrangements for child views. These arrangements are controlled by the
+ * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.</p>
+ *
+ * <p>Several default strategies are provided for common uses such as lists and grids.</p>
+ *
+ * @param layout LayoutManager to use
+ */
+ public void setLayoutManager(LayoutManager layout) {
+ if (layout == mLayout) {
+ return;
+ }
+ stopScroll();
+ // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
+ // chance that LayoutManagers will re-use views.
+ if (mLayout != null) {
+ // end all running animations
+ if (mItemAnimator != null) {
+ mItemAnimator.endAnimations();
+ }
+ mLayout.removeAndRecycleAllViews(mRecycler);
+ mLayout.removeAndRecycleScrapInt(mRecycler);
+ mRecycler.clear();
+
+ if (mIsAttached) {
+ mLayout.dispatchDetachedFromWindow(this, mRecycler);
+ }
+ mLayout.setRecyclerView(null);
+ mLayout = null;
+ } else {
+ mRecycler.clear();
+ }
+ // this is just a defensive measure for faulty item animators.
+ mChildHelper.removeAllViewsUnfiltered();
+ mLayout = layout;
+ if (layout != null) {
+ if (layout.mRecyclerView != null) {
+ throw new IllegalArgumentException("LayoutManager " + layout
+ + " is already attached to a RecyclerView: " + layout.mRecyclerView);
+ }
+ mLayout.setRecyclerView(this);
+ if (mIsAttached) {
+ mLayout.dispatchAttachedToWindow(this);
+ }
+ }
+ mRecycler.updateViewCacheSize();
+ requestLayout();
+ }
+
+ /**
+ * Set a {@link OnFlingListener} for this {@link RecyclerView}.
+ * <p>
+ * If the {@link OnFlingListener} is set then it will receive
+ * calls to {@link #fling(int,int)} and will be able to intercept them.
+ *
+ * @param onFlingListener The {@link OnFlingListener} instance.
+ */
+ public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) {
+ mOnFlingListener = onFlingListener;
+ }
+
+ /**
+ * Get the current {@link OnFlingListener} from this {@link RecyclerView}.
+ *
+ * @return The {@link OnFlingListener} instance currently set (can be null).
+ */
+ @Nullable
+ public OnFlingListener getOnFlingListener() {
+ return mOnFlingListener;
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ SavedState state = new SavedState(super.onSaveInstanceState());
+ if (mPendingSavedState != null) {
+ state.copyFrom(mPendingSavedState);
+ } else if (mLayout != null) {
+ state.mLayoutState = mLayout.onSaveInstanceState();
+ } else {
+ state.mLayoutState = null;
+ }
+
+ return state;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ mPendingSavedState = (SavedState) state;
+ super.onRestoreInstanceState(mPendingSavedState.getSuperState());
+ if (mLayout != null && mPendingSavedState.mLayoutState != null) {
+ mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
+ }
+ }
+
+ /**
+ * Override to prevent freezing of any views created by the adapter.
+ */
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ dispatchFreezeSelfOnly(container);
+ }
+
+ /**
+ * Override to prevent thawing of any views created by the adapter.
+ */
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ dispatchThawSelfOnly(container);
+ }
+
+ /**
+ * Adds a view to the animatingViews list.
+ * mAnimatingViews holds the child views that are currently being kept around
+ * purely for the purpose of being animated out of view. They are drawn as a regular
+ * part of the child list of the RecyclerView, but they are invisible to the LayoutManager
+ * as they are managed separately from the regular child views.
+ * @param viewHolder The ViewHolder to be removed
+ */
+ private void addAnimatingView(ViewHolder viewHolder) {
+ final View view = viewHolder.itemView;
+ final boolean alreadyParented = view.getParent() == this;
+ mRecycler.unscrapView(getChildViewHolder(view));
+ if (viewHolder.isTmpDetached()) {
+ // re-attach
+ mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
+ } else if (!alreadyParented) {
+ mChildHelper.addView(view, true);
+ } else {
+ mChildHelper.hide(view);
+ }
+ }
+
+ /**
+ * Removes a view from the animatingViews list.
+ * @param view The view to be removed
+ * @see #addAnimatingView(RecyclerView.ViewHolder)
+ * @return true if an animating view is removed
+ */
+ boolean removeAnimatingView(View view) {
+ eatRequestLayout();
+ final boolean removed = mChildHelper.removeViewIfHidden(view);
+ if (removed) {
+ final ViewHolder viewHolder = getChildViewHolderInt(view);
+ mRecycler.unscrapView(viewHolder);
+ mRecycler.recycleViewHolderInternal(viewHolder);
+ if (DEBUG) {
+ Log.d(TAG, "after removing animated view: " + view + ", " + this);
+ }
+ }
+ // only clear request eaten flag if we removed the view.
+ resumeRequestLayout(!removed);
+ return removed;
+ }
+
+ /**
+ * Return the {@link LayoutManager} currently responsible for
+ * layout policy for this RecyclerView.
+ *
+ * @return The currently bound LayoutManager
+ */
+ public LayoutManager getLayoutManager() {
+ return mLayout;
+ }
+
+ /**
+ * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null;
+ * if no pool is set for this view a new one will be created. See
+ * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information.
+ *
+ * @return The pool used to store recycled item views for reuse.
+ * @see #setRecycledViewPool(RecycledViewPool)
+ */
+ public RecycledViewPool getRecycledViewPool() {
+ return mRecycler.getRecycledViewPool();
+ }
+
+ /**
+ * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
+ * This can be useful if you have multiple RecyclerViews with adapters that use the same
+ * view types, for example if you have several data sets with the same kinds of item views
+ * displayed by a {@link android.support.v4.view.ViewPager ViewPager}.
+ *
+ * @param pool Pool to set. If this parameter is null a new pool will be created and used.
+ */
+ public void setRecycledViewPool(RecycledViewPool pool) {
+ mRecycler.setRecycledViewPool(pool);
+ }
+
+ /**
+ * Sets a new {@link ViewCacheExtension} to be used by the Recycler.
+ *
+ * @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
+ *
+ * @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)}
+ */
+ public void setViewCacheExtension(ViewCacheExtension extension) {
+ mRecycler.setViewCacheExtension(extension);
+ }
+
+ /**
+ * Set the number of offscreen views to retain before adding them to the potentially shared
+ * {@link #getRecycledViewPool() recycled view pool}.
+ *
+ * <p>The offscreen view cache stays aware of changes in the attached adapter, allowing
+ * a LayoutManager to reuse those views unmodified without needing to return to the adapter
+ * to rebind them.</p>
+ *
+ * @param size Number of views to cache offscreen before returning them to the general
+ * recycled view pool
+ */
+ public void setItemViewCacheSize(int size) {
+ mRecycler.setViewCacheSize(size);
+ }
+
+ /**
+ * Return the current scrolling state of the RecyclerView.
+ *
+ * @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or
+ * {@link #SCROLL_STATE_SETTLING}
+ */
+ public int getScrollState() {
+ return mScrollState;
+ }
+
+ void setScrollState(int state) {
+ if (state == mScrollState) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
+ new Exception());
+ }
+ mScrollState = state;
+ if (state != SCROLL_STATE_SETTLING) {
+ stopScrollersInternal();
+ }
+ dispatchOnScrollStateChanged(state);
+ }
+
+ /**
+ * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
+ * affect both measurement and drawing of individual item views.
+ *
+ * <p>Item decorations are ordered. Decorations placed earlier in the list will
+ * be run/queried/drawn first for their effects on item views. Padding added to views
+ * will be nested; a padding added by an earlier decoration will mean further
+ * item decorations in the list will be asked to draw/pad within the previous decoration's
+ * given area.</p>
+ *
+ * @param decor Decoration to add
+ * @param index Position in the decoration chain to insert this decoration at. If this value
+ * is negative the decoration will be added at the end.
+ */
+ public void addItemDecoration(ItemDecoration decor, int index) {
+ if (mLayout != null) {
+ mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or"
+ + " layout");
+ }
+ if (mItemDecorations.isEmpty()) {
+ setWillNotDraw(false);
+ }
+ if (index < 0) {
+ mItemDecorations.add(decor);
+ } else {
+ mItemDecorations.add(index, decor);
+ }
+ markItemDecorInsetsDirty();
+ requestLayout();
+ }
+
+ /**
+ * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
+ * affect both measurement and drawing of individual item views.
+ *
+ * <p>Item decorations are ordered. Decorations placed earlier in the list will
+ * be run/queried/drawn first for their effects on item views. Padding added to views
+ * will be nested; a padding added by an earlier decoration will mean further
+ * item decorations in the list will be asked to draw/pad within the previous decoration's
+ * given area.</p>
+ *
+ * @param decor Decoration to add
+ */
+ public void addItemDecoration(ItemDecoration decor) {
+ addItemDecoration(decor, -1);
+ }
+
+ /**
+ * Remove an {@link ItemDecoration} from this RecyclerView.
+ *
+ * <p>The given decoration will no longer impact the measurement and drawing of
+ * item views.</p>
+ *
+ * @param decor Decoration to remove
+ * @see #addItemDecoration(ItemDecoration)
+ */
+ public void removeItemDecoration(ItemDecoration decor) {
+ if (mLayout != null) {
+ mLayout.assertNotInLayoutOrScroll("Cannot remove item decoration during a scroll or"
+ + " layout");
+ }
+ mItemDecorations.remove(decor);
+ if (mItemDecorations.isEmpty()) {
+ setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
+ }
+ markItemDecorInsetsDirty();
+ requestLayout();
+ }
+
+ /**
+ * Sets the {@link ChildDrawingOrderCallback} to be used for drawing children.
+ * <p>
+ * See {@link ViewGroup#getChildDrawingOrder(int, int)} for details. Calling this method will
+ * always call {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean)}. The parameter will be
+ * true if childDrawingOrderCallback is not null, false otherwise.
+ * <p>
+ * Note that child drawing order may be overridden by View's elevation.
+ *
+ * @param childDrawingOrderCallback The ChildDrawingOrderCallback to be used by the drawing
+ * system.
+ */
+ public void setChildDrawingOrderCallback(ChildDrawingOrderCallback childDrawingOrderCallback) {
+ if (childDrawingOrderCallback == mChildDrawingOrderCallback) {
+ return;
+ }
+ mChildDrawingOrderCallback = childDrawingOrderCallback;
+ setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null);
+ }
+
+ /**
+ * Set a listener that will be notified of any changes in scroll state or position.
+ *
+ * @param listener Listener to set or null to clear
+ *
+ * @deprecated Use {@link #addOnScrollListener(OnScrollListener)} and
+ * {@link #removeOnScrollListener(OnScrollListener)}
+ */
+ @Deprecated
+ public void setOnScrollListener(OnScrollListener listener) {
+ mScrollListener = listener;
+ }
+
+ /**
+ * Add a listener that will be notified of any changes in scroll state or position.
+ *
+ * <p>Components that add a listener should take care to remove it when finished.
+ * Other components that take ownership of a view may call {@link #clearOnScrollListeners()}
+ * to remove all attached listeners.</p>
+ *
+ * @param listener listener to set or null to clear
+ */
+ public void addOnScrollListener(OnScrollListener listener) {
+ if (mScrollListeners == null) {
+ mScrollListeners = new ArrayList<>();
+ }
+ mScrollListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener that was notified of any changes in scroll state or position.
+ *
+ * @param listener listener to set or null to clear
+ */
+ public void removeOnScrollListener(OnScrollListener listener) {
+ if (mScrollListeners != null) {
+ mScrollListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Remove all secondary listener that were notified of any changes in scroll state or position.
+ */
+ public void clearOnScrollListeners() {
+ if (mScrollListeners != null) {
+ mScrollListeners.clear();
+ }
+ }
+
+ /**
+ * Convenience method to scroll to a certain position.
+ *
+ * RecyclerView does not implement scrolling logic, rather forwards the call to
+ * {@link com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
+ * @param position Scroll to this adapter position
+ * @see com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int)
+ */
+ public void scrollToPosition(int position) {
+ if (mLayoutFrozen) {
+ return;
+ }
+ stopScroll();
+ if (mLayout == null) {
+ Log.e(TAG, "Cannot scroll to position a LayoutManager set. "
+ + "Call setLayoutManager with a non-null argument.");
+ return;
+ }
+ mLayout.scrollToPosition(position);
+ awakenScrollBars();
+ }
+
+ void jumpToPositionForSmoothScroller(int position) {
+ if (mLayout == null) {
+ return;
+ }
+ mLayout.scrollToPosition(position);
+ awakenScrollBars();
+ }
+
+ /**
+ * Starts a smooth scroll to an adapter position.
+ * <p>
+ * To support smooth scrolling, you must override
+ * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
+ * {@link SmoothScroller}.
+ * <p>
+ * {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
+ * provide a custom smooth scroll logic, override
+ * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
+ * LayoutManager.
+ *
+ * @param position The adapter position to scroll to
+ * @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
+ */
+ public void smoothScrollToPosition(int position) {
+ if (mLayoutFrozen) {
+ return;
+ }
+ if (mLayout == null) {
+ Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+ + "Call setLayoutManager with a non-null argument.");
+ return;
+ }
+ mLayout.smoothScrollToPosition(this, mState, position);
+ }
+
+ @Override
+ public void scrollTo(int x, int y) {
+ Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. "
+ + "Use scrollToPosition instead");
+ }
+
+ @Override
+ public void scrollBy(int x, int y) {
+ if (mLayout == null) {
+ Log.e(TAG, "Cannot scroll without a LayoutManager set. "
+ + "Call setLayoutManager with a non-null argument.");
+ return;
+ }
+ if (mLayoutFrozen) {
+ return;
+ }
+ final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
+ final boolean canScrollVertical = mLayout.canScrollVertically();
+ if (canScrollHorizontal || canScrollVertical) {
+ scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
+ }
+ }
+
+ /**
+ * Helper method reflect data changes to the state.
+ * <p>
+ * Adapter changes during a scroll may trigger a crash because scroll assumes no data change
+ * but data actually changed.
+ * <p>
+ * This method consumes all deferred changes to avoid that case.
+ */
+ void consumePendingUpdateOperations() {
+ if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) {
+ Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
+ dispatchLayout();
+ Trace.endSection();
+ return;
+ }
+ if (!mAdapterHelper.hasPendingUpdates()) {
+ return;
+ }
+
+ // if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any
+ // of the visible items is affected and if not, just ignore the change.
+ if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper
+ .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE
+ | AdapterHelper.UpdateOp.MOVE)) {
+ Trace.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
+ eatRequestLayout();
+ onEnterLayoutOrScroll();
+ mAdapterHelper.preProcess();
+ if (!mLayoutRequestEaten) {
+ if (hasUpdatedView()) {
+ dispatchLayout();
+ } else {
+ // no need to layout, clean state
+ mAdapterHelper.consumePostponedUpdates();
+ }
+ }
+ resumeRequestLayout(true);
+ onExitLayoutOrScroll();
+ Trace.endSection();
+ } else if (mAdapterHelper.hasPendingUpdates()) {
+ Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
+ dispatchLayout();
+ Trace.endSection();
+ }
+ }
+
+ /**
+ * @return True if an existing view holder needs to be updated
+ */
+ private boolean hasUpdatedView() {
+ final int childCount = mChildHelper.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder == null || holder.shouldIgnore()) {
+ continue;
+ }
+ if (holder.isUpdated()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Does not perform bounds checking. Used by internal methods that have already validated input.
+ * <p>
+ * It also reports any unused scroll request to the related EdgeEffect.
+ *
+ * @param x The amount of horizontal scroll request
+ * @param y The amount of vertical scroll request
+ * @param ev The originating MotionEvent, or null if not from a touch event.
+ *
+ * @return Whether any scroll was consumed in either direction.
+ */
+ boolean scrollByInternal(int x, int y, MotionEvent ev) {
+ int unconsumedX = 0, unconsumedY = 0;
+ int consumedX = 0, consumedY = 0;
+
+ consumePendingUpdateOperations();
+ if (mAdapter != null) {
+ eatRequestLayout();
+ onEnterLayoutOrScroll();
+ Trace.beginSection(TRACE_SCROLL_TAG);
+ if (x != 0) {
+ consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
+ unconsumedX = x - consumedX;
+ }
+ if (y != 0) {
+ consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
+ unconsumedY = y - consumedY;
+ }
+ Trace.endSection();
+ repositionShadowingViews();
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
+ }
+ if (!mItemDecorations.isEmpty()) {
+ invalidate();
+ }
+
+ if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
+ // Update the last touch co-ords, taking any scroll offset into account
+ mLastTouchX -= mScrollOffset[0];
+ mLastTouchY -= mScrollOffset[1];
+ if (ev != null) {
+ ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
+ }
+ mNestedOffsets[0] += mScrollOffset[0];
+ mNestedOffsets[1] += mScrollOffset[1];
+ } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+ if (ev != null) {
+ pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
+ }
+ considerReleasingGlowsOnScroll(x, y);
+ }
+ if (consumedX != 0 || consumedY != 0) {
+ dispatchOnScrolled(consumedX, consumedY);
+ }
+ if (!awakenScrollBars()) {
+ invalidate();
+ }
+ return consumedX != 0 || consumedY != 0;
+ }
+
+ /**
+ * <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
+ * range. This value is used to compute the length of the thumb within the scrollbar's track.
+ * </p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link RecyclerView.LayoutManager#computeHorizontalScrollOffset(RecyclerView.State)} in your
+ * LayoutManager. </p>
+ *
+ * @return The horizontal offset of the scrollbar's thumb
+ * @see com.android.internal.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset
+ * (RecyclerView.State)
+ */
+ @Override
+ public int computeHorizontalScrollOffset() {
+ if (mLayout == null) {
+ return 0;
+ }
+ return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0;
+ }
+
+ /**
+ * <p>Compute the horizontal extent of the horizontal scrollbar's thumb within the
+ * horizontal range. This value is used to compute the length of the thumb within the
+ * scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)} in your
+ * LayoutManager.</p>
+ *
+ * @return The horizontal extent of the scrollbar's thumb
+ * @see RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)
+ */
+ @Override
+ public int computeHorizontalScrollExtent() {
+ if (mLayout == null) {
+ return 0;
+ }
+ return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollExtent(mState) : 0;
+ }
+
+ /**
+ * <p>Compute the horizontal range that the horizontal scrollbar represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your
+ * LayoutManager.</p>
+ *
+ * @return The total horizontal range represented by the vertical scrollbar
+ * @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)
+ */
+ @Override
+ public int computeHorizontalScrollRange() {
+ if (mLayout == null) {
+ return 0;
+ }
+ return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
+ }
+
+ /**
+ * <p>Compute the vertical offset of the vertical scrollbar's thumb within the vertical range.
+ * This value is used to compute the length of the thumb within the scrollbar's track. </p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your
+ * LayoutManager.</p>
+ *
+ * @return The vertical offset of the scrollbar's thumb
+ * @see com.android.internal.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset
+ * (RecyclerView.State)
+ */
+ @Override
+ public int computeVerticalScrollOffset() {
+ if (mLayout == null) {
+ return 0;
+ }
+ return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0;
+ }
+
+ /**
+ * <p>Compute the vertical extent of the vertical scrollbar's thumb within the vertical range.
+ * This value is used to compute the length of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)} in your
+ * LayoutManager.</p>
+ *
+ * @return The vertical extent of the scrollbar's thumb
+ * @see RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)
+ */
+ @Override
+ public int computeVerticalScrollExtent() {
+ if (mLayout == null) {
+ return 0;
+ }
+ return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollExtent(mState) : 0;
+ }
+
+ /**
+ * <p>Compute the vertical range that the vertical scrollbar represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)} in your
+ * LayoutManager.</p>
+ *
+ * @return The total vertical range represented by the vertical scrollbar
+ * @see RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)
+ */
+ @Override
+ public int computeVerticalScrollRange() {
+ if (mLayout == null) {
+ return 0;
+ }
+ return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0;
+ }
+
+
+ void eatRequestLayout() {
+ mEatRequestLayout++;
+ if (mEatRequestLayout == 1 && !mLayoutFrozen) {
+ mLayoutRequestEaten = false;
+ }
+ }
+
+ void resumeRequestLayout(boolean performLayoutChildren) {
+ if (mEatRequestLayout < 1) {
+ //noinspection PointlessBooleanExpression
+ if (DEBUG) {
+ throw new IllegalStateException("invalid eat request layout count");
+ }
+ mEatRequestLayout = 1;
+ }
+ if (!performLayoutChildren) {
+ // Reset the layout request eaten counter.
+ // This is necessary since eatRequest calls can be nested in which case the other
+ // call will override the inner one.
+ // for instance:
+ // eat layout for process adapter updates
+ // eat layout for dispatchLayout
+ // a bunch of req layout calls arrive
+
+ mLayoutRequestEaten = false;
+ }
+ if (mEatRequestLayout == 1) {
+ // when layout is frozen we should delay dispatchLayout()
+ if (performLayoutChildren && mLayoutRequestEaten && !mLayoutFrozen
+ && mLayout != null && mAdapter != null) {
+ dispatchLayout();
+ }
+ if (!mLayoutFrozen) {
+ mLayoutRequestEaten = false;
+ }
+ }
+ mEatRequestLayout--;
+ }
+
+ /**
+ * Enable or disable layout and scroll. After <code>setLayoutFrozen(true)</code> is called,
+ * Layout requests will be postponed until <code>setLayoutFrozen(false)</code> is called;
+ * child views are not updated when RecyclerView is frozen, {@link #smoothScrollBy(int, int)},
+ * {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and
+ * {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are
+ * dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be
+ * called.
+ *
+ * <p>
+ * <code>setLayoutFrozen(true)</code> does not prevent app from directly calling {@link
+ * LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition(
+ * RecyclerView, State, int)}.
+ * <p>
+ * {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically
+ * stop frozen.
+ * <p>
+ * Note: Running ItemAnimator is not stopped automatically, it's caller's
+ * responsibility to call ItemAnimator.end().
+ *
+ * @param frozen true to freeze layout and scroll, false to re-enable.
+ */
+ public void setLayoutFrozen(boolean frozen) {
+ if (frozen != mLayoutFrozen) {
+ assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll");
+ if (!frozen) {
+ mLayoutFrozen = false;
+ if (mLayoutRequestEaten && mLayout != null && mAdapter != null) {
+ requestLayout();
+ }
+ mLayoutRequestEaten = false;
+ } else {
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ onTouchEvent(cancelEvent);
+ mLayoutFrozen = true;
+ mIgnoreMotionEventTillDown = true;
+ stopScroll();
+ }
+ }
+ }
+
+ /**
+ * Returns true if layout and scroll are frozen.
+ *
+ * @return true if layout and scroll are frozen
+ * @see #setLayoutFrozen(boolean)
+ */
+ public boolean isLayoutFrozen() {
+ return mLayoutFrozen;
+ }
+
+ /**
+ * Animate a scroll by the given amount of pixels along either axis.
+ *
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ */
+ public void smoothScrollBy(int dx, int dy) {
+ smoothScrollBy(dx, dy, null);
+ }
+
+ /**
+ * Animate a scroll by the given amount of pixels along either axis.
+ *
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ * @param interpolator {@link Interpolator} to be used for scrolling. If it is
+ * {@code null}, RecyclerView is going to use the default interpolator.
+ */
+ public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
+ if (mLayout == null) {
+ Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+ + "Call setLayoutManager with a non-null argument.");
+ return;
+ }
+ if (mLayoutFrozen) {
+ return;
+ }
+ if (!mLayout.canScrollHorizontally()) {
+ dx = 0;
+ }
+ if (!mLayout.canScrollVertically()) {
+ dy = 0;
+ }
+ if (dx != 0 || dy != 0) {
+ mViewFlinger.smoothScrollBy(dx, dy, interpolator);
+ }
+ }
+
+ /**
+ * Begin a standard fling with an initial velocity along each axis in pixels per second.
+ * If the velocity given is below the system-defined minimum this method will return false
+ * and no fling will occur.
+ *
+ * @param velocityX Initial horizontal velocity in pixels per second
+ * @param velocityY Initial vertical velocity in pixels per second
+ * @return true if the fling was started, false if the velocity was too low to fling or
+ * LayoutManager does not support scrolling in the axis fling is issued.
+ *
+ * @see LayoutManager#canScrollVertically()
+ * @see LayoutManager#canScrollHorizontally()
+ */
+ public boolean fling(int velocityX, int velocityY) {
+ if (mLayout == null) {
+ Log.e(TAG, "Cannot fling without a LayoutManager set. "
+ + "Call setLayoutManager with a non-null argument.");
+ return false;
+ }
+ if (mLayoutFrozen) {
+ return false;
+ }
+
+ final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
+ final boolean canScrollVertical = mLayout.canScrollVertically();
+
+ if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
+ velocityX = 0;
+ }
+ if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
+ velocityY = 0;
+ }
+ if (velocityX == 0 && velocityY == 0) {
+ // If we don't have any velocity, return false
+ return false;
+ }
+
+ if (!dispatchNestedPreFling(velocityX, velocityY)) {
+ final View firstChild = mLayout.getChildAt(0);
+ final View lastChild = mLayout.getChildAt(mLayout.getChildCount() - 1);
+ boolean consumed = false;
+ if (velocityY < 0) {
+ consumed = getChildAdapterPosition(firstChild) > 0
+ || firstChild.getTop() < getPaddingTop();
+ }
+
+ if (velocityY > 0) {
+ consumed = getChildAdapterPosition(lastChild) < mAdapter.getItemCount() - 1
+ || lastChild.getBottom() > getHeight() - getPaddingBottom();
+ }
+
+ dispatchNestedFling(velocityX, velocityY, consumed);
+
+ if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
+ return true;
+ }
+
+ final boolean canScroll = canScrollHorizontal || canScrollVertical;
+
+ if (canScroll) {
+ velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
+ velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
+ mViewFlinger.fling(velocityX, velocityY);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Stop any current scroll in progress, such as one started by
+ * {@link #smoothScrollBy(int, int)}, {@link #fling(int, int)} or a touch-initiated fling.
+ */
+ public void stopScroll() {
+ setScrollState(SCROLL_STATE_IDLE);
+ stopScrollersInternal();
+ }
+
+ /**
+ * Similar to {@link #stopScroll()} but does not set the state.
+ */
+ private void stopScrollersInternal() {
+ mViewFlinger.stop();
+ if (mLayout != null) {
+ mLayout.stopSmoothScroller();
+ }
+ }
+
+ /**
+ * Returns the minimum velocity to start a fling.
+ *
+ * @return The minimum velocity to start a fling
+ */
+ public int getMinFlingVelocity() {
+ return mMinFlingVelocity;
+ }
+
+
+ /**
+ * Returns the maximum fling velocity used by this RecyclerView.
+ *
+ * @return The maximum fling velocity used by this RecyclerView.
+ */
+ public int getMaxFlingVelocity() {
+ return mMaxFlingVelocity;
+ }
+
+ /**
+ * Apply a pull to relevant overscroll glow effects
+ */
+ private void pullGlows(float x, float overscrollX, float y, float overscrollY) {
+ boolean invalidate = false;
+ if (overscrollX < 0) {
+ ensureLeftGlow();
+ mLeftGlow.onPull(-overscrollX / getWidth(), 1f - y / getHeight());
+ invalidate = true;
+ } else if (overscrollX > 0) {
+ ensureRightGlow();
+ mRightGlow.onPull(overscrollX / getWidth(), y / getHeight());
+ invalidate = true;
+ }
+
+ if (overscrollY < 0) {
+ ensureTopGlow();
+ mTopGlow.onPull(-overscrollY / getHeight(), x / getWidth());
+ invalidate = true;
+ } else if (overscrollY > 0) {
+ ensureBottomGlow();
+ mBottomGlow.onPull(overscrollY / getHeight(), 1f - x / getWidth());
+ invalidate = true;
+ }
+
+ if (invalidate || overscrollX != 0 || overscrollY != 0) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ private void releaseGlows() {
+ boolean needsInvalidate = false;
+ if (mLeftGlow != null) {
+ mLeftGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (mTopGlow != null) {
+ mTopGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (mRightGlow != null) {
+ mRightGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (mBottomGlow != null) {
+ mBottomGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (needsInvalidate) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ void considerReleasingGlowsOnScroll(int dx, int dy) {
+ boolean needsInvalidate = false;
+ if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) {
+ mLeftGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) {
+ mRightGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) {
+ mTopGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) {
+ mBottomGlow.onRelease();
+ needsInvalidate = true;
+ }
+ if (needsInvalidate) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ void absorbGlows(int velocityX, int velocityY) {
+ if (velocityX < 0) {
+ ensureLeftGlow();
+ mLeftGlow.onAbsorb(-velocityX);
+ } else if (velocityX > 0) {
+ ensureRightGlow();
+ mRightGlow.onAbsorb(velocityX);
+ }
+
+ if (velocityY < 0) {
+ ensureTopGlow();
+ mTopGlow.onAbsorb(-velocityY);
+ } else if (velocityY > 0) {
+ ensureBottomGlow();
+ mBottomGlow.onAbsorb(velocityY);
+ }
+
+ if (velocityX != 0 || velocityY != 0) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ void ensureLeftGlow() {
+ if (mLeftGlow != null) {
+ return;
+ }
+ mLeftGlow = new EdgeEffect(getContext());
+ if (mClipToPadding) {
+ mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+ } else {
+ mLeftGlow.setSize(getMeasuredHeight(), getMeasuredWidth());
+ }
+ }
+
+ void ensureRightGlow() {
+ if (mRightGlow != null) {
+ return;
+ }
+ mRightGlow = new EdgeEffect(getContext());
+ if (mClipToPadding) {
+ mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+ } else {
+ mRightGlow.setSize(getMeasuredHeight(), getMeasuredWidth());
+ }
+ }
+
+ void ensureTopGlow() {
+ if (mTopGlow != null) {
+ return;
+ }
+ mTopGlow = new EdgeEffect(getContext());
+ if (mClipToPadding) {
+ mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+ } else {
+ mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
+ }
+
+ }
+
+ void ensureBottomGlow() {
+ if (mBottomGlow != null) {
+ return;
+ }
+ mBottomGlow = new EdgeEffect(getContext());
+ if (mClipToPadding) {
+ mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+ } else {
+ mBottomGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
+ }
+ }
+
+ void invalidateGlows() {
+ mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null;
+ }
+
+ /**
+ * Since RecyclerView is a collection ViewGroup that includes virtual children (items that are
+ * in the Adapter but not visible in the UI), it employs a more involved focus search strategy
+ * that differs from other ViewGroups.
+ * <p>
+ * It first does a focus search within the RecyclerView. If this search finds a View that is in
+ * the focus direction with respect to the currently focused View, RecyclerView returns that
+ * child as the next focus target. When it cannot find such child, it calls
+ * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} to layout more Views
+ * in the focus search direction. If LayoutManager adds a View that matches the
+ * focus search criteria, it will be returned as the focus search result. Otherwise,
+ * RecyclerView will call parent to handle the focus search like a regular ViewGroup.
+ * <p>
+ * When the direction is {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD}, a View that
+ * is not in the focus direction is still valid focus target which may not be the desired
+ * behavior if the Adapter has more children in the focus direction. To handle this case,
+ * RecyclerView converts the focus direction to an absolute direction and makes a preliminary
+ * focus search in that direction. If there are no Views to gain focus, it will call
+ * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} before running a
+ * focus search with the original (relative) direction. This allows RecyclerView to provide
+ * better candidates to the focus search while still allowing the view system to take focus from
+ * the RecyclerView and give it to a more suitable child if such child exists.
+ *
+ * @param focused The view that currently has focus
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, {@link View#FOCUS_FORWARD},
+ * {@link View#FOCUS_BACKWARD} or 0 for not applicable.
+ *
+ * @return A new View that can be the next focus after the focused View
+ */
+ @Override
+ public View focusSearch(View focused, int direction) {
+ View result = mLayout.onInterceptFocusSearch(focused, direction);
+ if (result != null) {
+ return result;
+ }
+ final boolean canRunFocusFailure = mAdapter != null && mLayout != null
+ && !isComputingLayout() && !mLayoutFrozen;
+
+ final FocusFinder ff = FocusFinder.getInstance();
+ if (canRunFocusFailure
+ && (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) {
+ // convert direction to absolute direction and see if we have a view there and if not
+ // tell LayoutManager to add if it can.
+ boolean needsFocusFailureLayout = false;
+ if (mLayout.canScrollVertically()) {
+ final int absDir =
+ direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
+ final View found = ff.findNextFocus(this, focused, absDir);
+ needsFocusFailureLayout = found == null;
+ if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
+ // Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
+ direction = absDir;
+ }
+ }
+ if (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) {
+ boolean rtl = mLayout.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
+ ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+ final View found = ff.findNextFocus(this, focused, absDir);
+ needsFocusFailureLayout = found == null;
+ if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
+ // Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
+ direction = absDir;
+ }
+ }
+ if (needsFocusFailureLayout) {
+ consumePendingUpdateOperations();
+ final View focusedItemView = findContainingItemView(focused);
+ if (focusedItemView == null) {
+ // panic, focused view is not a child anymore, cannot call super.
+ return null;
+ }
+ eatRequestLayout();
+ mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
+ resumeRequestLayout(false);
+ }
+ result = ff.findNextFocus(this, focused, direction);
+ } else {
+ result = ff.findNextFocus(this, focused, direction);
+ if (result == null && canRunFocusFailure) {
+ consumePendingUpdateOperations();
+ final View focusedItemView = findContainingItemView(focused);
+ if (focusedItemView == null) {
+ // panic, focused view is not a child anymore, cannot call super.
+ return null;
+ }
+ eatRequestLayout();
+ result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
+ resumeRequestLayout(false);
+ }
+ }
+ return isPreferredNextFocus(focused, result, direction)
+ ? result : super.focusSearch(focused, direction);
+ }
+
+ /**
+ * Checks if the new focus candidate is a good enough candidate such that RecyclerView will
+ * assign it as the next focus View instead of letting view hierarchy decide.
+ * A good candidate means a View that is aligned in the focus direction wrt the focused View
+ * and is not the RecyclerView itself.
+ * When this method returns false, RecyclerView will let the parent make the decision so the
+ * same View may still get the focus as a result of that search.
+ */
+ private boolean isPreferredNextFocus(View focused, View next, int direction) {
+ if (next == null || next == this) {
+ return false;
+ }
+ if (focused == null) {
+ return true;
+ }
+
+ if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
+ final boolean rtl = mLayout.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ final int absHorizontal = (direction == View.FOCUS_FORWARD) ^ rtl
+ ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+ if (isPreferredNextFocusAbsolute(focused, next, absHorizontal)) {
+ return true;
+ }
+ if (direction == View.FOCUS_FORWARD) {
+ return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_DOWN);
+ } else {
+ return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_UP);
+ }
+ } else {
+ return isPreferredNextFocusAbsolute(focused, next, direction);
+ }
+
+ }
+
+ /**
+ * Logic taken from FocusSearch#isCandidate
+ */
+ private boolean isPreferredNextFocusAbsolute(View focused, View next, int direction) {
+ mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
+ mTempRect2.set(0, 0, next.getWidth(), next.getHeight());
+ offsetDescendantRectToMyCoords(focused, mTempRect);
+ offsetDescendantRectToMyCoords(next, mTempRect2);
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return (mTempRect.right > mTempRect2.right
+ || mTempRect.left >= mTempRect2.right)
+ && mTempRect.left > mTempRect2.left;
+ case View.FOCUS_RIGHT:
+ return (mTempRect.left < mTempRect2.left
+ || mTempRect.right <= mTempRect2.left)
+ && mTempRect.right < mTempRect2.right;
+ case View.FOCUS_UP:
+ return (mTempRect.bottom > mTempRect2.bottom
+ || mTempRect.top >= mTempRect2.bottom)
+ && mTempRect.top > mTempRect2.top;
+ case View.FOCUS_DOWN:
+ return (mTempRect.top < mTempRect2.top
+ || mTempRect.bottom <= mTempRect2.top)
+ && mTempRect.bottom < mTempRect2.bottom;
+ }
+ throw new IllegalArgumentException("direction must be absolute. received:" + direction);
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) {
+ mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
+
+ // get item decor offsets w/o refreshing. If they are invalid, there will be another
+ // layout pass to fix them, then it is LayoutManager's responsibility to keep focused
+ // View in viewport.
+ final ViewGroup.LayoutParams focusedLayoutParams = focused.getLayoutParams();
+ if (focusedLayoutParams instanceof LayoutParams) {
+ // if focused child has item decors, use them. Otherwise, ignore.
+ final LayoutParams lp = (LayoutParams) focusedLayoutParams;
+ if (!lp.mInsetsDirty) {
+ final Rect insets = lp.mDecorInsets;
+ mTempRect.left -= insets.left;
+ mTempRect.right += insets.right;
+ mTempRect.top -= insets.top;
+ mTempRect.bottom += insets.bottom;
+ }
+ }
+
+ offsetDescendantRectToMyCoords(focused, mTempRect);
+ offsetRectIntoDescendantCoords(child, mTempRect);
+ requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete);
+ }
+ super.requestChildFocus(child, focused);
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+ return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate);
+ }
+
+ @Override
+ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+ if (mLayout == null || !mLayout.onAddFocusables(this, views, direction, focusableMode)) {
+ super.addFocusables(views, direction, focusableMode);
+ }
+ }
+
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ if (isComputingLayout()) {
+ // if we are in the middle of a layout calculation, don't let any child take focus.
+ // RV will handle it after layout calculation is finished.
+ return false;
+ }
+ return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mLayoutOrScrollCounter = 0;
+ mIsAttached = true;
+ mFirstLayoutComplete = mFirstLayoutComplete && !isLayoutRequested();
+ if (mLayout != null) {
+ mLayout.dispatchAttachedToWindow(this);
+ }
+ mPostedAnimatorRunner = false;
+
+ if (ALLOW_THREAD_GAP_WORK) {
+ // Register with gap worker
+ mGapWorker = GapWorker.sGapWorker.get();
+ if (mGapWorker == null) {
+ mGapWorker = new GapWorker();
+
+ // break 60 fps assumption if data from display appears valid
+ // NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
+ Display display = getDisplay();
+ float refreshRate = 60.0f;
+ if (!isInEditMode() && display != null) {
+ float displayRefreshRate = display.getRefreshRate();
+ if (displayRefreshRate >= 30.0f) {
+ refreshRate = displayRefreshRate;
+ }
+ }
+ mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
+ GapWorker.sGapWorker.set(mGapWorker);
+ }
+ mGapWorker.add(this);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mItemAnimator != null) {
+ mItemAnimator.endAnimations();
+ }
+ stopScroll();
+ mIsAttached = false;
+ if (mLayout != null) {
+ mLayout.dispatchDetachedFromWindow(this, mRecycler);
+ }
+ mPendingAccessibilityImportanceChange.clear();
+ removeCallbacks(mItemAnimatorRunner);
+ mViewInfoStore.onDetach();
+
+ if (ALLOW_THREAD_GAP_WORK) {
+ // Unregister with gap worker
+ mGapWorker.remove(this);
+ mGapWorker = null;
+ }
+ }
+
+ /**
+ * Returns true if RecyclerView is attached to window.
+ */
+ // @override
+ public boolean isAttachedToWindow() {
+ return mIsAttached;
+ }
+
+ /**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is not</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertNotInLayoutOrScroll(String)
+ */
+ void assertInLayoutOrScroll(String message) {
+ if (!isComputingLayout()) {
+ if (message == null) {
+ throw new IllegalStateException("Cannot call this method unless RecyclerView is "
+ + "computing a layout or scrolling");
+ }
+ throw new IllegalStateException(message);
+
+ }
+ }
+
+ /**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertInLayoutOrScroll(String)
+ */
+ void assertNotInLayoutOrScroll(String message) {
+ if (isComputingLayout()) {
+ if (message == null) {
+ throw new IllegalStateException("Cannot call this method while RecyclerView is "
+ + "computing a layout or scrolling");
+ }
+ throw new IllegalStateException(message);
+ }
+ if (mDispatchScrollCounter > 0) {
+ Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might be run"
+ + " during a measure & layout pass where you cannot change the RecyclerView"
+ + " data. Any method call that might change the structure of the RecyclerView"
+ + " or the adapter contents should be postponed to the next frame.",
+ new IllegalStateException(""));
+ }
+ }
+
+ /**
+ * Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched
+ * to child views or this view's standard scrolling behavior.
+ *
+ * <p>Client code may use listeners to implement item manipulation behavior. Once a listener
+ * returns true from
+ * {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its
+ * {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called
+ * for each incoming MotionEvent until the end of the gesture.</p>
+ *
+ * @param listener Listener to add
+ * @see SimpleOnItemTouchListener
+ */
+ public void addOnItemTouchListener(OnItemTouchListener listener) {
+ mOnItemTouchListeners.add(listener);
+ }
+
+ /**
+ * Remove an {@link OnItemTouchListener}. It will no longer be able to intercept touch events.
+ *
+ * @param listener Listener to remove
+ */
+ public void removeOnItemTouchListener(OnItemTouchListener listener) {
+ mOnItemTouchListeners.remove(listener);
+ if (mActiveOnItemTouchListener == listener) {
+ mActiveOnItemTouchListener = null;
+ }
+ }
+
+ private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
+ final int action = e.getAction();
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
+ mActiveOnItemTouchListener = null;
+ }
+
+ final int listenerCount = mOnItemTouchListeners.size();
+ for (int i = 0; i < listenerCount; i++) {
+ final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+ if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
+ mActiveOnItemTouchListener = listener;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean dispatchOnItemTouch(MotionEvent e) {
+ final int action = e.getAction();
+ if (mActiveOnItemTouchListener != null) {
+ if (action == MotionEvent.ACTION_DOWN) {
+ // Stale state from a previous gesture, we're starting a new one. Clear it.
+ mActiveOnItemTouchListener = null;
+ } else {
+ mActiveOnItemTouchListener.onTouchEvent(this, e);
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ // Clean up for the next gesture.
+ mActiveOnItemTouchListener = null;
+ }
+ return true;
+ }
+ }
+
+ // Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept
+ // as called from onInterceptTouchEvent; skip it.
+ if (action != MotionEvent.ACTION_DOWN) {
+ final int listenerCount = mOnItemTouchListeners.size();
+ for (int i = 0; i < listenerCount; i++) {
+ final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+ if (listener.onInterceptTouchEvent(this, e)) {
+ mActiveOnItemTouchListener = listener;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent e) {
+ if (mLayoutFrozen) {
+ // When layout is frozen, RV does not intercept the motion event.
+ // A child view e.g. a button may still get the click.
+ return false;
+ }
+ if (dispatchOnItemTouchIntercept(e)) {
+ cancelTouch();
+ return true;
+ }
+
+ if (mLayout == null) {
+ return false;
+ }
+
+ final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
+ final boolean canScrollVertically = mLayout.canScrollVertically();
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(e);
+
+ final int action = e.getActionMasked();
+ final int actionIndex = e.getActionIndex();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (mIgnoreMotionEventTillDown) {
+ mIgnoreMotionEventTillDown = false;
+ }
+ mScrollPointerId = e.getPointerId(0);
+ mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+
+ if (mScrollState == SCROLL_STATE_SETTLING) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+
+ // Clear the nested offsets
+ mNestedOffsets[0] = mNestedOffsets[1] = 0;
+
+ int nestedScrollAxis = View.SCROLL_AXIS_NONE;
+ if (canScrollHorizontally) {
+ nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL;
+ }
+ if (canScrollVertically) {
+ nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL;
+ }
+ startNestedScroll(nestedScrollAxis);
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ mScrollPointerId = e.getPointerId(actionIndex);
+ mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = e.findPointerIndex(mScrollPointerId);
+ if (index < 0) {
+ Log.e(TAG, "Error processing scroll; pointer index for id "
+ + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
+ return false;
+ }
+
+ final int x = (int) (e.getX(index) + 0.5f);
+ final int y = (int) (e.getY(index) + 0.5f);
+ if (mScrollState != SCROLL_STATE_DRAGGING) {
+ final int dx = x - mInitialTouchX;
+ final int dy = y - mInitialTouchY;
+ boolean startScroll = false;
+ if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+ mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+ mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ if (startScroll) {
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ }
+ } break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onPointerUp(e);
+ } break;
+
+ case MotionEvent.ACTION_UP: {
+ mVelocityTracker.clear();
+ stopNestedScroll();
+ } break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ cancelTouch();
+ }
+ }
+ return mScrollState == SCROLL_STATE_DRAGGING;
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ final int listenerCount = mOnItemTouchListeners.size();
+ for (int i = 0; i < listenerCount; i++) {
+ final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+ listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent e) {
+ if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
+ return false;
+ }
+ if (dispatchOnItemTouch(e)) {
+ cancelTouch();
+ return true;
+ }
+
+ if (mLayout == null) {
+ return false;
+ }
+
+ final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
+ final boolean canScrollVertically = mLayout.canScrollVertically();
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ boolean eventAddedToVelocityTracker = false;
+
+ final MotionEvent vtev = MotionEvent.obtain(e);
+ final int action = e.getActionMasked();
+ final int actionIndex = e.getActionIndex();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mNestedOffsets[0] = mNestedOffsets[1] = 0;
+ }
+ vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mScrollPointerId = e.getPointerId(0);
+ mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+
+ int nestedScrollAxis = View.SCROLL_AXIS_NONE;
+ if (canScrollHorizontally) {
+ nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL;
+ }
+ if (canScrollVertically) {
+ nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL;
+ }
+ startNestedScroll(nestedScrollAxis);
+ } break;
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ mScrollPointerId = e.getPointerId(actionIndex);
+ mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
+ } break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = e.findPointerIndex(mScrollPointerId);
+ if (index < 0) {
+ Log.e(TAG, "Error processing scroll; pointer index for id "
+ + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
+ return false;
+ }
+
+ final int x = (int) (e.getX(index) + 0.5f);
+ final int y = (int) (e.getY(index) + 0.5f);
+ int dx = mLastTouchX - x;
+ int dy = mLastTouchY - y;
+
+ if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
+ dx -= mScrollConsumed[0];
+ dy -= mScrollConsumed[1];
+ vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
+ // Updated the nested offsets
+ mNestedOffsets[0] += mScrollOffset[0];
+ mNestedOffsets[1] += mScrollOffset[1];
+ }
+
+ if (mScrollState != SCROLL_STATE_DRAGGING) {
+ boolean startScroll = false;
+ if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+ if (dx > 0) {
+ dx -= mTouchSlop;
+ } else {
+ dx += mTouchSlop;
+ }
+ startScroll = true;
+ }
+ if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+ if (dy > 0) {
+ dy -= mTouchSlop;
+ } else {
+ dy += mTouchSlop;
+ }
+ startScroll = true;
+ }
+ if (startScroll) {
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ }
+
+ if (mScrollState == SCROLL_STATE_DRAGGING) {
+ mLastTouchX = x - mScrollOffset[0];
+ mLastTouchY = y - mScrollOffset[1];
+
+ if (scrollByInternal(
+ canScrollHorizontally ? dx : 0,
+ canScrollVertically ? dy : 0,
+ vtev)) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+ if (mGapWorker != null && (dx != 0 || dy != 0)) {
+ mGapWorker.postFromTraversal(this, dx, dy);
+ }
+ }
+ } break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onPointerUp(e);
+ } break;
+
+ case MotionEvent.ACTION_UP: {
+ mVelocityTracker.addMovement(vtev);
+ eventAddedToVelocityTracker = true;
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
+ final float xvel = canScrollHorizontally
+ ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
+ final float yvel = canScrollVertically
+ ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
+ if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
+ setScrollState(SCROLL_STATE_IDLE);
+ }
+ resetTouch();
+ } break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ cancelTouch();
+ } break;
+ }
+
+ if (!eventAddedToVelocityTracker) {
+ mVelocityTracker.addMovement(vtev);
+ }
+ vtev.recycle();
+
+ return true;
+ }
+
+ private void resetTouch() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ stopNestedScroll();
+ releaseGlows();
+ }
+
+ private void cancelTouch() {
+ resetTouch();
+ setScrollState(SCROLL_STATE_IDLE);
+ }
+
+ private void onPointerUp(MotionEvent e) {
+ final int actionIndex = e.getActionIndex();
+ if (e.getPointerId(actionIndex) == mScrollPointerId) {
+ // Pick a new pointer to pick up the slack.
+ final int newIndex = actionIndex == 0 ? 1 : 0;
+ mScrollPointerId = e.getPointerId(newIndex);
+ mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
+ }
+ }
+
+ // @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (mLayout == null) {
+ return false;
+ }
+ if (mLayoutFrozen) {
+ return false;
+ }
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ if (event.getAction() == MotionEvent.ACTION_SCROLL) {
+ final float vScroll, hScroll;
+ if (mLayout.canScrollVertically()) {
+ // Inverse the sign of the vertical scroll to align the scroll orientation
+ // with AbsListView.
+ vScroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ } else {
+ vScroll = 0f;
+ }
+ if (mLayout.canScrollHorizontally()) {
+ hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ } else {
+ hScroll = 0f;
+ }
+
+ if (vScroll != 0 || hScroll != 0) {
+ final float scrollFactor = getScrollFactor();
+ scrollByInternal((int) (hScroll * scrollFactor),
+ (int) (vScroll * scrollFactor), event);
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Ported from View.getVerticalScrollFactor.
+ */
+ private float getScrollFactor() {
+ if (mScrollFactor == Float.MIN_VALUE) {
+ TypedValue outValue = new TypedValue();
+ if (getContext().getTheme().resolveAttribute(
+ android.R.attr.listPreferredItemHeight, outValue, true)) {
+ mScrollFactor = outValue.getDimension(
+ getContext().getResources().getDisplayMetrics());
+ } else {
+ return 0; //listPreferredItemHeight is not defined, no generic scrolling
+ }
+ }
+ return mScrollFactor;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ if (mLayout == null) {
+ defaultOnMeasure(widthSpec, heightSpec);
+ return;
+ }
+ if (mLayout.mAutoMeasure) {
+ final int widthMode = MeasureSpec.getMode(widthSpec);
+ final int heightMode = MeasureSpec.getMode(heightSpec);
+ final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
+ && heightMode == MeasureSpec.EXACTLY;
+ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+ if (skipMeasure || mAdapter == null) {
+ return;
+ }
+ if (mState.mLayoutStep == State.STEP_START) {
+ dispatchLayoutStep1();
+ }
+ // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
+ // consistency
+ mLayout.setMeasureSpecs(widthSpec, heightSpec);
+ mState.mIsMeasuring = true;
+ dispatchLayoutStep2();
+
+ // now we can get the width and height from the children.
+ mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
+
+ // if RecyclerView has non-exact width and height and if there is at least one child
+ // which also has non-exact width & height, we have to re-measure.
+ if (mLayout.shouldMeasureTwice()) {
+ mLayout.setMeasureSpecs(
+ MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
+ mState.mIsMeasuring = true;
+ dispatchLayoutStep2();
+ // now we can get the width and height from the children.
+ mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
+ }
+ } else {
+ if (mHasFixedSize) {
+ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+ return;
+ }
+ // custom onMeasure
+ if (mAdapterUpdateDuringMeasure) {
+ eatRequestLayout();
+ onEnterLayoutOrScroll();
+ processAdapterUpdatesAndSetAnimationFlags();
+ onExitLayoutOrScroll();
+
+ if (mState.mRunPredictiveAnimations) {
+ mState.mInPreLayout = true;
+ } else {
+ // consume remaining updates to provide a consistent state with the layout pass.
+ mAdapterHelper.consumeUpdatesInOnePass();
+ mState.mInPreLayout = false;
+ }
+ mAdapterUpdateDuringMeasure = false;
+ resumeRequestLayout(false);
+ }
+
+ if (mAdapter != null) {
+ mState.mItemCount = mAdapter.getItemCount();
+ } else {
+ mState.mItemCount = 0;
+ }
+ eatRequestLayout();
+ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+ resumeRequestLayout(false);
+ mState.mInPreLayout = false; // clear
+ }
+ }
+
+ /**
+ * Used when onMeasure is called before layout manager is set
+ */
+ void defaultOnMeasure(int widthSpec, int heightSpec) {
+ // calling LayoutManager here is not pretty but that API is already public and it is better
+ // than creating another method since this is internal.
+ final int width = LayoutManager.chooseSize(widthSpec,
+ getPaddingLeft() + getPaddingRight(),
+ getMinimumWidth());
+ final int height = LayoutManager.chooseSize(heightSpec,
+ getPaddingTop() + getPaddingBottom(),
+ getMinimumHeight());
+
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (w != oldw || h != oldh) {
+ invalidateGlows();
+ // layout's w/h are updated during measure/layout steps.
+ }
+ }
+
+ /**
+ * Sets the {@link ItemAnimator} that will handle animations involving changes
+ * to the items in this RecyclerView. By default, RecyclerView instantiates and
+ * uses an instance of {@link DefaultItemAnimator}. Whether item animations are
+ * enabled for the RecyclerView depends on the ItemAnimator and whether
+ * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations()
+ * supports item animations}.
+ *
+ * @param animator The ItemAnimator being set. If null, no animations will occur
+ * when changes occur to the items in this RecyclerView.
+ */
+ public void setItemAnimator(ItemAnimator animator) {
+ if (mItemAnimator != null) {
+ mItemAnimator.endAnimations();
+ mItemAnimator.setListener(null);
+ }
+ mItemAnimator = animator;
+ if (mItemAnimator != null) {
+ mItemAnimator.setListener(mItemAnimatorListener);
+ }
+ }
+
+ void onEnterLayoutOrScroll() {
+ mLayoutOrScrollCounter++;
+ }
+
+ void onExitLayoutOrScroll() {
+ mLayoutOrScrollCounter--;
+ if (mLayoutOrScrollCounter < 1) {
+ if (DEBUG && mLayoutOrScrollCounter < 0) {
+ throw new IllegalStateException("layout or scroll counter cannot go below zero."
+ + "Some calls are not matching");
+ }
+ mLayoutOrScrollCounter = 0;
+ dispatchContentChangedIfNecessary();
+ dispatchPendingImportantForAccessibilityChanges();
+ }
+ }
+
+ boolean isAccessibilityEnabled() {
+ return mAccessibilityManager != null && mAccessibilityManager.isEnabled();
+ }
+
+ private void dispatchContentChangedIfNecessary() {
+ final int flags = mEatenAccessibilityChangeFlags;
+ mEatenAccessibilityChangeFlags = 0;
+ if (flags != 0 && isAccessibilityEnabled()) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(flags);
+ sendAccessibilityEventUnchecked(event);
+ }
+ }
+
+ /**
+ * Returns whether RecyclerView is currently computing a layout.
+ * <p>
+ * If this method returns true, it means that RecyclerView is in a lockdown state and any
+ * attempt to update adapter contents will result in an exception because adapter contents
+ * cannot be changed while RecyclerView is trying to compute the layout.
+ * <p>
+ * It is very unlikely that your code will be running during this state as it is
+ * called by the framework when a layout traversal happens or RecyclerView starts to scroll
+ * in response to system events (touch, accessibility etc).
+ * <p>
+ * This case may happen if you have some custom logic to change adapter contents in
+ * response to a View callback (e.g. focus change callback) which might be triggered during a
+ * layout calculation. In these cases, you should just postpone the change using a Handler or a
+ * similar mechanism.
+ *
+ * @return <code>true</code> if RecyclerView is currently computing a layout, <code>false</code>
+ * otherwise
+ */
+ public boolean isComputingLayout() {
+ return mLayoutOrScrollCounter > 0;
+ }
+
+ /**
+ * Returns true if an accessibility event should not be dispatched now. This happens when an
+ * accessibility request arrives while RecyclerView does not have a stable state which is very
+ * hard to handle for a LayoutManager. Instead, this method records necessary information about
+ * the event and dispatches a window change event after the critical section is finished.
+ *
+ * @return True if the accessibility event should be postponed.
+ */
+ boolean shouldDeferAccessibilityEvent(AccessibilityEvent event) {
+ if (isComputingLayout()) {
+ int type = 0;
+ if (event != null) {
+ type = event.getContentChangeTypes();
+ }
+ if (type == 0) {
+ type = AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
+ }
+ mEatenAccessibilityChangeFlags |= type;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+ if (shouldDeferAccessibilityEvent(event)) {
+ return;
+ }
+ super.sendAccessibilityEventUnchecked(event);
+ }
+
+ /**
+ * Gets the current ItemAnimator for this RecyclerView. A null return value
+ * indicates that there is no animator and that item changes will happen without
+ * any animations. By default, RecyclerView instantiates and
+ * uses an instance of {@link DefaultItemAnimator}.
+ *
+ * @return ItemAnimator The current ItemAnimator. If null, no animations will occur
+ * when changes occur to the items in this RecyclerView.
+ */
+ public ItemAnimator getItemAnimator() {
+ return mItemAnimator;
+ }
+
+ /**
+ * Post a runnable to the next frame to run pending item animations. Only the first such
+ * request will be posted, governed by the mPostedAnimatorRunner flag.
+ */
+ void postAnimationRunner() {
+ if (!mPostedAnimatorRunner && mIsAttached) {
+ postOnAnimation(mItemAnimatorRunner);
+ mPostedAnimatorRunner = true;
+ }
+ }
+
+ private boolean predictiveItemAnimationsEnabled() {
+ return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
+ }
+
+ /**
+ * Consumes adapter updates and calculates which type of animations we want to run.
+ * Called in onMeasure and dispatchLayout.
+ * <p>
+ * This method may process only the pre-layout state of updates or all of them.
+ */
+ private void processAdapterUpdatesAndSetAnimationFlags() {
+ if (mDataSetHasChangedAfterLayout) {
+ // Processing these items have no value since data set changed unexpectedly.
+ // Instead, we just reset it.
+ mAdapterHelper.reset();
+ mLayout.onItemsChanged(this);
+ }
+ // simple animations are a subset of advanced animations (which will cause a
+ // pre-layout step)
+ // If layout supports predictive animations, pre-process to decide if we want to run them
+ if (predictiveItemAnimationsEnabled()) {
+ mAdapterHelper.preProcess();
+ } else {
+ mAdapterHelper.consumeUpdatesInOnePass();
+ }
+ boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
+ mState.mRunSimpleAnimations = mFirstLayoutComplete
+ && mItemAnimator != null
+ && (mDataSetHasChangedAfterLayout
+ || animationTypeSupported
+ || mLayout.mRequestedSimpleAnimations)
+ && (!mDataSetHasChangedAfterLayout
+ || mAdapter.hasStableIds());
+ mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
+ && animationTypeSupported
+ && !mDataSetHasChangedAfterLayout
+ && predictiveItemAnimationsEnabled();
+ }
+
+ /**
+ * Wrapper around layoutChildren() that handles animating changes caused by layout.
+ * Animations work on the assumption that there are five different kinds of items
+ * in play:
+ * PERSISTENT: items are visible before and after layout
+ * REMOVED: items were visible before layout and were removed by the app
+ * ADDED: items did not exist before layout and were added by the app
+ * DISAPPEARING: items exist in the data set before/after, but changed from
+ * visible to non-visible in the process of layout (they were moved off
+ * screen as a side-effect of other changes)
+ * APPEARING: items exist in the data set before/after, but changed from
+ * non-visible to visible in the process of layout (they were moved on
+ * screen as a side-effect of other changes)
+ * The overall approach figures out what items exist before/after layout and
+ * infers one of the five above states for each of the items. Then the animations
+ * are set up accordingly:
+ * PERSISTENT views are animated via
+ * {@link ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
+ * DISAPPEARING views are animated via
+ * {@link ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
+ * APPEARING views are animated via
+ * {@link ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)}
+ * and changed views are animated via
+ * {@link ItemAnimator#animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)}.
+ */
+ void dispatchLayout() {
+ if (mAdapter == null) {
+ Log.e(TAG, "No adapter attached; skipping layout");
+ // leave the state in START
+ return;
+ }
+ if (mLayout == null) {
+ Log.e(TAG, "No layout manager attached; skipping layout");
+ // leave the state in START
+ return;
+ }
+ mState.mIsMeasuring = false;
+ if (mState.mLayoutStep == State.STEP_START) {
+ dispatchLayoutStep1();
+ mLayout.setExactMeasureSpecsFrom(this);
+ dispatchLayoutStep2();
+ } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
+ || mLayout.getHeight() != getHeight()) {
+ // First 2 steps are done in onMeasure but looks like we have to run again due to
+ // changed size.
+ mLayout.setExactMeasureSpecsFrom(this);
+ dispatchLayoutStep2();
+ } else {
+ // always make sure we sync them (to ensure mode is exact)
+ mLayout.setExactMeasureSpecsFrom(this);
+ }
+ dispatchLayoutStep3();
+ }
+
+ private void saveFocusInfo() {
+ View child = null;
+ if (mPreserveFocusAfterLayout && hasFocus() && mAdapter != null) {
+ child = getFocusedChild();
+ }
+
+ final ViewHolder focusedVh = child == null ? null : findContainingViewHolder(child);
+ if (focusedVh == null) {
+ resetFocusInfo();
+ } else {
+ mState.mFocusedItemId = mAdapter.hasStableIds() ? focusedVh.getItemId() : NO_ID;
+ // mFocusedItemPosition should hold the current adapter position of the previously
+ // focused item. If the item is removed, we store the previous adapter position of the
+ // removed item.
+ mState.mFocusedItemPosition = mDataSetHasChangedAfterLayout ? NO_POSITION
+ : (focusedVh.isRemoved() ? focusedVh.mOldPosition
+ : focusedVh.getAdapterPosition());
+ mState.mFocusedSubChildId = getDeepestFocusedViewWithId(focusedVh.itemView);
+ }
+ }
+
+ private void resetFocusInfo() {
+ mState.mFocusedItemId = NO_ID;
+ mState.mFocusedItemPosition = NO_POSITION;
+ mState.mFocusedSubChildId = View.NO_ID;
+ }
+
+ /**
+ * Finds the best view candidate to request focus on using mFocusedItemPosition index of the
+ * previously focused item. It first traverses the adapter forward to find a focusable candidate
+ * and if no such candidate is found, it reverses the focus search direction for the items
+ * before the mFocusedItemPosition'th index;
+ * @return The best candidate to request focus on, or null if no such candidate exists. Null
+ * indicates all the existing adapter items are unfocusable.
+ */
+ @Nullable
+ private View findNextViewToFocus() {
+ int startFocusSearchIndex = mState.mFocusedItemPosition != -1 ? mState.mFocusedItemPosition
+ : 0;
+ ViewHolder nextFocus;
+ final int itemCount = mState.getItemCount();
+ for (int i = startFocusSearchIndex; i < itemCount; i++) {
+ nextFocus = findViewHolderForAdapterPosition(i);
+ if (nextFocus == null) {
+ break;
+ }
+ if (nextFocus.itemView.hasFocusable()) {
+ return nextFocus.itemView;
+ }
+ }
+ final int limit = Math.min(itemCount, startFocusSearchIndex);
+ for (int i = limit - 1; i >= 0; i--) {
+ nextFocus = findViewHolderForAdapterPosition(i);
+ if (nextFocus == null) {
+ return null;
+ }
+ if (nextFocus.itemView.hasFocusable()) {
+ return nextFocus.itemView;
+ }
+ }
+ return null;
+ }
+
+ private void recoverFocusFromState() {
+ if (!mPreserveFocusAfterLayout || mAdapter == null || !hasFocus()
+ || getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS
+ || (getDescendantFocusability() == FOCUS_BEFORE_DESCENDANTS && isFocused())) {
+ // No-op if either of these cases happens:
+ // 1. RV has no focus, or 2. RV blocks focus to its children, or 3. RV takes focus
+ // before its children and is focused (i.e. it already stole the focus away from its
+ // descendants).
+ return;
+ }
+ // only recover focus if RV itself has the focus or the focused view is hidden
+ if (!isFocused()) {
+ final View focusedChild = getFocusedChild();
+ if (IGNORE_DETACHED_FOCUSED_CHILD
+ && (focusedChild.getParent() == null || !focusedChild.hasFocus())) {
+ // Special handling of API 15-. A focused child can be invalid because mFocus is not
+ // cleared when the child is detached (mParent = null),
+ // This happens because clearFocus on API 15- does not invalidate mFocus of its
+ // parent when this child is detached.
+ // For API 16+, this is not an issue because requestFocus takes care of clearing the
+ // prior detached focused child. For API 15- the problem happens in 2 cases because
+ // clearChild does not call clearChildFocus on RV: 1. setFocusable(false) is called
+ // for the current focused item which calls clearChild or 2. when the prior focused
+ // child is removed, removeDetachedView called in layout step 3 which calls
+ // clearChild. We should ignore this invalid focused child in all our calculations
+ // for the next view to receive focus, and apply the focus recovery logic instead.
+ if (mChildHelper.getChildCount() == 0) {
+ // No children left. Request focus on the RV itself since one of its children
+ // was holding focus previously.
+ requestFocus();
+ return;
+ }
+ } else if (!mChildHelper.isHidden(focusedChild)) {
+ // If the currently focused child is hidden, apply the focus recovery logic.
+ // Otherwise return, i.e. the currently (unhidden) focused child is good enough :/.
+ return;
+ }
+ }
+ ViewHolder focusTarget = null;
+ // RV first attempts to locate the previously focused item to request focus on using
+ // mFocusedItemId. If such an item no longer exists, it then makes a best-effort attempt to
+ // find the next best candidate to request focus on based on mFocusedItemPosition.
+ if (mState.mFocusedItemId != NO_ID && mAdapter.hasStableIds()) {
+ focusTarget = findViewHolderForItemId(mState.mFocusedItemId);
+ }
+ View viewToFocus = null;
+ if (focusTarget == null || mChildHelper.isHidden(focusTarget.itemView)
+ || !focusTarget.itemView.hasFocusable()) {
+ if (mChildHelper.getChildCount() > 0) {
+ // At this point, RV has focus and either of these conditions are true:
+ // 1. There's no previously focused item either because RV received focused before
+ // layout, or the previously focused item was removed, or RV doesn't have stable IDs
+ // 2. Previous focus child is hidden, or 3. Previous focused child is no longer
+ // focusable. In either of these cases, we make sure that RV still passes down the
+ // focus to one of its focusable children using a best-effort algorithm.
+ viewToFocus = findNextViewToFocus();
+ }
+ } else {
+ // looks like the focused item has been replaced with another view that represents the
+ // same item in the adapter. Request focus on that.
+ viewToFocus = focusTarget.itemView;
+ }
+
+ if (viewToFocus != null) {
+ if (mState.mFocusedSubChildId != NO_ID) {
+ View child = viewToFocus.findViewById(mState.mFocusedSubChildId);
+ if (child != null && child.isFocusable()) {
+ viewToFocus = child;
+ }
+ }
+ viewToFocus.requestFocus();
+ }
+ }
+
+ private int getDeepestFocusedViewWithId(View view) {
+ int lastKnownId = view.getId();
+ while (!view.isFocused() && view instanceof ViewGroup && view.hasFocus()) {
+ view = ((ViewGroup) view).getFocusedChild();
+ final int id = view.getId();
+ if (id != View.NO_ID) {
+ lastKnownId = view.getId();
+ }
+ }
+ return lastKnownId;
+ }
+
+ /**
+ * The first step of a layout where we;
+ * - process adapter updates
+ * - decide which animation should run
+ * - save information about current views
+ * - If necessary, run predictive layout and save its information
+ */
+ private void dispatchLayoutStep1() {
+ mState.assertLayoutStep(State.STEP_START);
+ mState.mIsMeasuring = false;
+ eatRequestLayout();
+ mViewInfoStore.clear();
+ onEnterLayoutOrScroll();
+ processAdapterUpdatesAndSetAnimationFlags();
+ saveFocusInfo();
+ mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
+ mItemsAddedOrRemoved = mItemsChanged = false;
+ mState.mInPreLayout = mState.mRunPredictiveAnimations;
+ mState.mItemCount = mAdapter.getItemCount();
+ findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
+
+ if (mState.mRunSimpleAnimations) {
+ // Step 0: Find out where all non-removed items are, pre-layout
+ int count = mChildHelper.getChildCount();
+ for (int i = 0; i < count; ++i) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
+ continue;
+ }
+ final ItemHolderInfo animationInfo = mItemAnimator
+ .recordPreLayoutInformation(mState, holder,
+ ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
+ holder.getUnmodifiedPayloads());
+ mViewInfoStore.addToPreLayout(holder, animationInfo);
+ if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
+ && !holder.shouldIgnore() && !holder.isInvalid()) {
+ long key = getChangedHolderKey(holder);
+ // This is NOT the only place where a ViewHolder is added to old change holders
+ // list. There is another case where:
+ // * A VH is currently hidden but not deleted
+ // * The hidden item is changed in the adapter
+ // * Layout manager decides to layout the item in the pre-Layout pass (step1)
+ // When this case is detected, RV will un-hide that view and add to the old
+ // change holders list.
+ mViewInfoStore.addToOldChangeHolders(key, holder);
+ }
+ }
+ }
+ if (mState.mRunPredictiveAnimations) {
+ // Step 1: run prelayout: This will use the old positions of items. The layout manager
+ // is expected to layout everything, even removed items (though not to add removed
+ // items back to the container). This gives the pre-layout position of APPEARING views
+ // which come into existence as part of the real layout.
+
+ // Save old positions so that LayoutManager can run its mapping logic.
+ saveOldPositions();
+ final boolean didStructureChange = mState.mStructureChanged;
+ mState.mStructureChanged = false;
+ // temporarily disable flag because we are asking for previous layout
+ mLayout.onLayoutChildren(mRecycler, mState);
+ mState.mStructureChanged = didStructureChange;
+
+ for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
+ final View child = mChildHelper.getChildAt(i);
+ final ViewHolder viewHolder = getChildViewHolderInt(child);
+ if (viewHolder.shouldIgnore()) {
+ continue;
+ }
+ if (!mViewInfoStore.isInPreLayout(viewHolder)) {
+ int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
+ boolean wasHidden = viewHolder
+ .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ if (!wasHidden) {
+ flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+ }
+ final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
+ mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
+ if (wasHidden) {
+ recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
+ } else {
+ mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
+ }
+ }
+ }
+ // we don't process disappearing list because they may re-appear in post layout pass.
+ clearOldPositions();
+ } else {
+ clearOldPositions();
+ }
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
+ mState.mLayoutStep = State.STEP_LAYOUT;
+ }
+
+ /**
+ * The second layout step where we do the actual layout of the views for the final state.
+ * This step might be run multiple times if necessary (e.g. measure).
+ */
+ private void dispatchLayoutStep2() {
+ eatRequestLayout();
+ onEnterLayoutOrScroll();
+ mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
+ mAdapterHelper.consumeUpdatesInOnePass();
+ mState.mItemCount = mAdapter.getItemCount();
+ mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
+
+ // Step 2: Run layout
+ mState.mInPreLayout = false;
+ mLayout.onLayoutChildren(mRecycler, mState);
+
+ mState.mStructureChanged = false;
+ mPendingSavedState = null;
+
+ // onLayoutChildren may have caused client code to disable item animations; re-check
+ mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
+ mState.mLayoutStep = State.STEP_ANIMATIONS;
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
+ }
+
+ /**
+ * The final step of the layout where we save the information about views for animations,
+ * trigger animations and do any necessary cleanup.
+ */
+ private void dispatchLayoutStep3() {
+ mState.assertLayoutStep(State.STEP_ANIMATIONS);
+ eatRequestLayout();
+ onEnterLayoutOrScroll();
+ mState.mLayoutStep = State.STEP_START;
+ if (mState.mRunSimpleAnimations) {
+ // Step 3: Find out where things are now, and process change animations.
+ // traverse list in reverse because we may call animateChange in the loop which may
+ // remove the target view holder.
+ for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
+ ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder.shouldIgnore()) {
+ continue;
+ }
+ long key = getChangedHolderKey(holder);
+ final ItemHolderInfo animationInfo = mItemAnimator
+ .recordPostLayoutInformation(mState, holder);
+ ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
+ if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
+ // run a change animation
+
+ // If an Item is CHANGED but the updated version is disappearing, it creates
+ // a conflicting case.
+ // Since a view that is marked as disappearing is likely to be going out of
+ // bounds, we run a change animation. Both views will be cleaned automatically
+ // once their animations finish.
+ // On the other hand, if it is the same view holder instance, we run a
+ // disappearing animation instead because we are not going to rebind the updated
+ // VH unless it is enforced by the layout manager.
+ final boolean oldDisappearing = mViewInfoStore.isDisappearing(
+ oldChangeViewHolder);
+ final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
+ if (oldDisappearing && oldChangeViewHolder == holder) {
+ // run disappear animation instead of change
+ mViewInfoStore.addToPostLayout(holder, animationInfo);
+ } else {
+ final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
+ oldChangeViewHolder);
+ // we add and remove so that any post info is merged.
+ mViewInfoStore.addToPostLayout(holder, animationInfo);
+ ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
+ if (preInfo == null) {
+ handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
+ } else {
+ animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
+ oldDisappearing, newDisappearing);
+ }
+ }
+ } else {
+ mViewInfoStore.addToPostLayout(holder, animationInfo);
+ }
+ }
+
+ // Step 4: Process view info lists and trigger animations
+ mViewInfoStore.process(mViewInfoProcessCallback);
+ }
+
+ mLayout.removeAndRecycleScrapInt(mRecycler);
+ mState.mPreviousLayoutItemCount = mState.mItemCount;
+ mDataSetHasChangedAfterLayout = false;
+ mState.mRunSimpleAnimations = false;
+
+ mState.mRunPredictiveAnimations = false;
+ mLayout.mRequestedSimpleAnimations = false;
+ if (mRecycler.mChangedScrap != null) {
+ mRecycler.mChangedScrap.clear();
+ }
+ if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
+ // Initial prefetch has expanded cache, so reset until next prefetch.
+ // This prevents initial prefetches from expanding the cache permanently.
+ mLayout.mPrefetchMaxCountObserved = 0;
+ mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
+ mRecycler.updateViewCacheSize();
+ }
+
+ mLayout.onLayoutCompleted(mState);
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
+ mViewInfoStore.clear();
+ if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
+ dispatchOnScrolled(0, 0);
+ }
+ recoverFocusFromState();
+ resetFocusInfo();
+ }
+
+ /**
+ * This handles the case where there is an unexpected VH missing in the pre-layout map.
+ * <p>
+ * We might be able to detect the error in the application which will help the developer to
+ * resolve the issue.
+ * <p>
+ * If it is not an expected error, we at least print an error to notify the developer and ignore
+ * the animation.
+ *
+ * https://code.google.com/p/android/issues/detail?id=193958
+ *
+ * @param key The change key
+ * @param holder Current ViewHolder
+ * @param oldChangeViewHolder Changed ViewHolder
+ */
+ private void handleMissingPreInfoForChangeError(long key,
+ ViewHolder holder, ViewHolder oldChangeViewHolder) {
+ // check if two VH have the same key, if so, print that as an error
+ final int childCount = mChildHelper.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View view = mChildHelper.getChildAt(i);
+ ViewHolder other = getChildViewHolderInt(view);
+ if (other == holder) {
+ continue;
+ }
+ final long otherKey = getChangedHolderKey(other);
+ if (otherKey == key) {
+ if (mAdapter != null && mAdapter.hasStableIds()) {
+ throw new IllegalStateException("Two different ViewHolders have the same stable"
+ + " ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT"
+ + " change.\n ViewHolder 1:" + other + " \n View Holder 2:" + holder);
+ } else {
+ throw new IllegalStateException("Two different ViewHolders have the same change"
+ + " ID. This might happen due to inconsistent Adapter update events or"
+ + " if the LayoutManager lays out the same View multiple times."
+ + "\n ViewHolder 1:" + other + " \n View Holder 2:" + holder);
+ }
+ }
+ }
+ // Very unlikely to happen but if it does, notify the developer.
+ Log.e(TAG, "Problem while matching changed view holders with the new"
+ + "ones. The pre-layout information for the change holder " + oldChangeViewHolder
+ + " cannot be found but it is necessary for " + holder);
+ }
+
+ /**
+ * Records the animation information for a view holder that was bounced from hidden list. It
+ * also clears the bounce back flag.
+ */
+ void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder,
+ ItemHolderInfo animationInfo) {
+ // looks like this view bounced back from hidden list!
+ viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ if (mState.mTrackOldChangeHolders && viewHolder.isUpdated()
+ && !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) {
+ long key = getChangedHolderKey(viewHolder);
+ mViewInfoStore.addToOldChangeHolders(key, viewHolder);
+ }
+ mViewInfoStore.addToPreLayout(viewHolder, animationInfo);
+ }
+
+ private void findMinMaxChildLayoutPositions(int[] into) {
+ final int count = mChildHelper.getChildCount();
+ if (count == 0) {
+ into[0] = NO_POSITION;
+ into[1] = NO_POSITION;
+ return;
+ }
+ int minPositionPreLayout = Integer.MAX_VALUE;
+ int maxPositionPreLayout = Integer.MIN_VALUE;
+ for (int i = 0; i < count; ++i) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder.shouldIgnore()) {
+ continue;
+ }
+ final int pos = holder.getLayoutPosition();
+ if (pos < minPositionPreLayout) {
+ minPositionPreLayout = pos;
+ }
+ if (pos > maxPositionPreLayout) {
+ maxPositionPreLayout = pos;
+ }
+ }
+ into[0] = minPositionPreLayout;
+ into[1] = maxPositionPreLayout;
+ }
+
+ private boolean didChildRangeChange(int minPositionPreLayout, int maxPositionPreLayout) {
+ findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
+ return mMinMaxLayoutPositions[0] != minPositionPreLayout
+ || mMinMaxLayoutPositions[1] != maxPositionPreLayout;
+ }
+
+ @Override
+ protected void removeDetachedView(View child, boolean animate) {
+ ViewHolder vh = getChildViewHolderInt(child);
+ if (vh != null) {
+ if (vh.isTmpDetached()) {
+ vh.clearTmpDetachFlag();
+ } else if (!vh.shouldIgnore()) {
+ throw new IllegalArgumentException("Called removeDetachedView with a view which"
+ + " is not flagged as tmp detached." + vh);
+ }
+ }
+ dispatchChildDetached(child);
+ super.removeDetachedView(child, animate);
+ }
+
+ /**
+ * Returns a unique key to be used while handling change animations.
+ * It might be child's position or stable id depending on the adapter type.
+ */
+ long getChangedHolderKey(ViewHolder holder) {
+ return mAdapter.hasStableIds() ? holder.getItemId() : holder.mPosition;
+ }
+
+ void animateAppearance(@NonNull ViewHolder itemHolder,
+ @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
+ itemHolder.setIsRecyclable(false);
+ if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
+ postAnimationRunner();
+ }
+ }
+
+ void animateDisappearance(@NonNull ViewHolder holder,
+ @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
+ addAnimatingView(holder);
+ holder.setIsRecyclable(false);
+ if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
+ postAnimationRunner();
+ }
+ }
+
+ private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
+ @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,
+ boolean oldHolderDisappearing, boolean newHolderDisappearing) {
+ oldHolder.setIsRecyclable(false);
+ if (oldHolderDisappearing) {
+ addAnimatingView(oldHolder);
+ }
+ if (oldHolder != newHolder) {
+ if (newHolderDisappearing) {
+ addAnimatingView(newHolder);
+ }
+ oldHolder.mShadowedHolder = newHolder;
+ // old holder should disappear after animation ends
+ addAnimatingView(oldHolder);
+ mRecycler.unscrapView(oldHolder);
+ newHolder.setIsRecyclable(false);
+ newHolder.mShadowingHolder = oldHolder;
+ }
+ if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {
+ postAnimationRunner();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ Trace.beginSection(TRACE_ON_LAYOUT_TAG);
+ dispatchLayout();
+ Trace.endSection();
+ mFirstLayoutComplete = true;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (mEatRequestLayout == 0 && !mLayoutFrozen) {
+ super.requestLayout();
+ } else {
+ mLayoutRequestEaten = true;
+ }
+ }
+
+ void markItemDecorInsetsDirty() {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = mChildHelper.getUnfilteredChildAt(i);
+ ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
+ }
+ mRecycler.markItemDecorInsetsDirty();
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ super.draw(c);
+
+ final int count = mItemDecorations.size();
+ for (int i = 0; i < count; i++) {
+ mItemDecorations.get(i).onDrawOver(c, this, mState);
+ }
+ // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
+ // need find children closest to edges. Not sure if it is worth the effort.
+ boolean needsInvalidate = false;
+ if (mLeftGlow != null && !mLeftGlow.isFinished()) {
+ final int restore = c.save();
+ final int padding = mClipToPadding ? getPaddingBottom() : 0;
+ c.rotate(270);
+ c.translate(-getHeight() + padding, 0);
+ needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+ if (mTopGlow != null && !mTopGlow.isFinished()) {
+ final int restore = c.save();
+ if (mClipToPadding) {
+ c.translate(getPaddingLeft(), getPaddingTop());
+ }
+ needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+ if (mRightGlow != null && !mRightGlow.isFinished()) {
+ final int restore = c.save();
+ final int width = getWidth();
+ final int padding = mClipToPadding ? getPaddingTop() : 0;
+ c.rotate(90);
+ c.translate(-padding, -width);
+ needsInvalidate |= mRightGlow != null && mRightGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+ if (mBottomGlow != null && !mBottomGlow.isFinished()) {
+ final int restore = c.save();
+ c.rotate(180);
+ if (mClipToPadding) {
+ c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom());
+ } else {
+ c.translate(-getWidth(), -getHeight());
+ }
+ needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+
+ // If some views are animating, ItemDecorators are likely to move/change with them.
+ // Invalidate RecyclerView to re-draw decorators. This is still efficient because children's
+ // display lists are not invalidated.
+ if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0
+ && mItemAnimator.isRunning()) {
+ needsInvalidate = true;
+ }
+
+ if (needsInvalidate) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ super.onDraw(c);
+
+ final int count = mItemDecorations.size();
+ for (int i = 0; i < count; i++) {
+ mItemDecorations.get(i).onDraw(c, this, mState);
+ }
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ if (mLayout == null) {
+ throw new IllegalStateException("RecyclerView has no LayoutManager");
+ }
+ return mLayout.generateDefaultLayoutParams();
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ if (mLayout == null) {
+ throw new IllegalStateException("RecyclerView has no LayoutManager");
+ }
+ return mLayout.generateLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (mLayout == null) {
+ throw new IllegalStateException("RecyclerView has no LayoutManager");
+ }
+ return mLayout.generateLayoutParams(p);
+ }
+
+ /**
+ * Returns true if RecyclerView is currently running some animations.
+ * <p>
+ * If you want to be notified when animations are finished, use
+ * {@link ItemAnimator#isRunning(ItemAnimator.ItemAnimatorFinishedListener)}.
+ *
+ * @return True if there are some item animations currently running or waiting to be started.
+ */
+ public boolean isAnimating() {
+ return mItemAnimator != null && mItemAnimator.isRunning();
+ }
+
+ void saveOldPositions() {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (DEBUG && holder.mPosition == -1 && !holder.isRemoved()) {
+ throw new IllegalStateException("view holder cannot have position -1 unless it"
+ + " is removed");
+ }
+ if (!holder.shouldIgnore()) {
+ holder.saveOldPosition();
+ }
+ }
+ }
+
+ void clearOldPositions() {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (!holder.shouldIgnore()) {
+ holder.clearOldPosition();
+ }
+ }
+ mRecycler.clearOldPositions();
+ }
+
+ void offsetPositionRecordsForMove(int from, int to) {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ final int start, end, inBetweenOffset;
+ if (from < to) {
+ start = from;
+ end = to;
+ inBetweenOffset = -1;
+ } else {
+ start = to;
+ end = from;
+ inBetweenOffset = 1;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder == null || holder.mPosition < start || holder.mPosition > end) {
+ continue;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForMove attached child " + i + " holder "
+ + holder);
+ }
+ if (holder.mPosition == from) {
+ holder.offsetPosition(to - from, false);
+ } else {
+ holder.offsetPosition(inBetweenOffset, false);
+ }
+
+ mState.mStructureChanged = true;
+ }
+ mRecycler.offsetPositionRecordsForMove(from, to);
+ requestLayout();
+ }
+
+ void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder "
+ + holder + " now at position " + (holder.mPosition + itemCount));
+ }
+ holder.offsetPosition(itemCount, false);
+ mState.mStructureChanged = true;
+ }
+ }
+ mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
+ requestLayout();
+ }
+
+ void offsetPositionRecordsForRemove(int positionStart, int itemCount,
+ boolean applyToPreLayout) {
+ final int positionEnd = positionStart + itemCount;
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.shouldIgnore()) {
+ if (holder.mPosition >= positionEnd) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
+ + " holder " + holder + " now at position "
+ + (holder.mPosition - itemCount));
+ }
+ holder.offsetPosition(-itemCount, applyToPreLayout);
+ mState.mStructureChanged = true;
+ } else if (holder.mPosition >= positionStart) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
+ + " holder " + holder + " now REMOVED");
+ }
+ holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
+ applyToPreLayout);
+ mState.mStructureChanged = true;
+ }
+ }
+ }
+ mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
+ requestLayout();
+ }
+
+ /**
+ * Rebind existing views for the given range, or create as needed.
+ *
+ * @param positionStart Adapter position to start at
+ * @param itemCount Number of views that must explicitly be rebound
+ */
+ void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ final int positionEnd = positionStart + itemCount;
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = mChildHelper.getUnfilteredChildAt(i);
+ final ViewHolder holder = getChildViewHolderInt(child);
+ if (holder == null || holder.shouldIgnore()) {
+ continue;
+ }
+ if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
+ // We re-bind these view holders after pre-processing is complete so that
+ // ViewHolders have their final positions assigned.
+ holder.addFlags(ViewHolder.FLAG_UPDATE);
+ holder.addChangePayload(payload);
+ // lp cannot be null since we get ViewHolder from it.
+ ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
+ }
+ }
+ mRecycler.viewRangeUpdate(positionStart, itemCount);
+ }
+
+ boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
+ return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
+ viewHolder.getUnmodifiedPayloads());
+ }
+
+
+ /**
+ * Call this method to signal that *all* adapter content has changed (generally, because of
+ * swapAdapter, or notifyDataSetChanged), and that once layout occurs, all attached items should
+ * be discarded or animated. Note that this work is deferred because RecyclerView requires a
+ * layout to resolve non-incremental changes to the data set.
+ *
+ * Attached items are labeled as position unknown, and may no longer be cached.
+ *
+ * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true,
+ * so calling this method *must* be associated with marking the cache invalid, so that the
+ * only valid items that remain in the cache, once layout occurs, are prefetched items.
+ */
+ void setDataSetChangedAfterLayout() {
+ if (mDataSetHasChangedAfterLayout) {
+ return;
+ }
+ mDataSetHasChangedAfterLayout = true;
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.shouldIgnore()) {
+ holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
+ }
+ }
+ mRecycler.setAdapterPositionsAsUnknown();
+
+ // immediately mark all views as invalid, so prefetched views can be
+ // differentiated from views bound to previous data set - both in children, and cache
+ markKnownViewsInvalid();
+ }
+
+ /**
+ * Mark all known views as invalid. Used in response to a, "the whole world might have changed"
+ * data change event.
+ */
+ void markKnownViewsInvalid() {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.shouldIgnore()) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ }
+ }
+ markItemDecorInsetsDirty();
+ mRecycler.markKnownViewsInvalid();
+ }
+
+ /**
+ * Invalidates all ItemDecorations. If RecyclerView has item decorations, calling this method
+ * will trigger a {@link #requestLayout()} call.
+ */
+ public void invalidateItemDecorations() {
+ if (mItemDecorations.size() == 0) {
+ return;
+ }
+ if (mLayout != null) {
+ mLayout.assertNotInLayoutOrScroll("Cannot invalidate item decorations during a scroll"
+ + " or layout");
+ }
+ markItemDecorInsetsDirty();
+ requestLayout();
+ }
+
+ /**
+ * Returns true if the RecyclerView should attempt to preserve currently focused Adapter Item's
+ * focus even if the View representing the Item is replaced during a layout calculation.
+ * <p>
+ * By default, this value is {@code true}.
+ *
+ * @return True if the RecyclerView will try to preserve focused Item after a layout if it loses
+ * focus.
+ *
+ * @see #setPreserveFocusAfterLayout(boolean)
+ */
+ public boolean getPreserveFocusAfterLayout() {
+ return mPreserveFocusAfterLayout;
+ }
+
+ /**
+ * Set whether the RecyclerView should try to keep the same Item focused after a layout
+ * calculation or not.
+ * <p>
+ * Usually, LayoutManagers keep focused views visible before and after layout but sometimes,
+ * views may lose focus during a layout calculation as their state changes or they are replaced
+ * with another view due to type change or animation. In these cases, RecyclerView can request
+ * focus on the new view automatically.
+ *
+ * @param preserveFocusAfterLayout Whether RecyclerView should preserve focused Item during a
+ * layout calculations. Defaults to true.
+ *
+ * @see #getPreserveFocusAfterLayout()
+ */
+ public void setPreserveFocusAfterLayout(boolean preserveFocusAfterLayout) {
+ mPreserveFocusAfterLayout = preserveFocusAfterLayout;
+ }
+
+ /**
+ * Retrieve the {@link ViewHolder} for the given child view.
+ *
+ * @param child Child of this RecyclerView to query for its ViewHolder
+ * @return The child view's ViewHolder
+ */
+ public ViewHolder getChildViewHolder(View child) {
+ final ViewParent parent = child.getParent();
+ if (parent != null && parent != this) {
+ throw new IllegalArgumentException("View " + child + " is not a direct child of "
+ + this);
+ }
+ return getChildViewHolderInt(child);
+ }
+
+ /**
+ * Traverses the ancestors of the given view and returns the item view that contains it and
+ * also a direct child of the RecyclerView. This returned view can be used to get the
+ * ViewHolder by calling {@link #getChildViewHolder(View)}.
+ *
+ * @param view The view that is a descendant of the RecyclerView.
+ *
+ * @return The direct child of the RecyclerView which contains the given view or null if the
+ * provided view is not a descendant of this RecyclerView.
+ *
+ * @see #getChildViewHolder(View)
+ * @see #findContainingViewHolder(View)
+ */
+ @Nullable
+ public View findContainingItemView(View view) {
+ ViewParent parent = view.getParent();
+ while (parent != null && parent != this && parent instanceof View) {
+ view = (View) parent;
+ parent = view.getParent();
+ }
+ return parent == this ? view : null;
+ }
+
+ /**
+ * Returns the ViewHolder that contains the given view.
+ *
+ * @param view The view that is a descendant of the RecyclerView.
+ *
+ * @return The ViewHolder that contains the given view or null if the provided view is not a
+ * descendant of this RecyclerView.
+ */
+ @Nullable
+ public ViewHolder findContainingViewHolder(View view) {
+ View itemView = findContainingItemView(view);
+ return itemView == null ? null : getChildViewHolder(itemView);
+ }
+
+
+ static ViewHolder getChildViewHolderInt(View child) {
+ if (child == null) {
+ return null;
+ }
+ return ((LayoutParams) child.getLayoutParams()).mViewHolder;
+ }
+
+ /**
+ * @deprecated use {@link #getChildAdapterPosition(View)} or
+ * {@link #getChildLayoutPosition(View)}.
+ */
+ @Deprecated
+ public int getChildPosition(View child) {
+ return getChildAdapterPosition(child);
+ }
+
+ /**
+ * Return the adapter position that the given child view corresponds to.
+ *
+ * @param child Child View to query
+ * @return Adapter position corresponding to the given view or {@link #NO_POSITION}
+ */
+ public int getChildAdapterPosition(View child) {
+ final ViewHolder holder = getChildViewHolderInt(child);
+ return holder != null ? holder.getAdapterPosition() : NO_POSITION;
+ }
+
+ /**
+ * Return the adapter position of the given child view as of the latest completed layout pass.
+ * <p>
+ * This position may not be equal to Item's adapter position if there are pending changes
+ * in the adapter which have not been reflected to the layout yet.
+ *
+ * @param child Child View to query
+ * @return Adapter position of the given View as of last layout pass or {@link #NO_POSITION} if
+ * the View is representing a removed item.
+ */
+ public int getChildLayoutPosition(View child) {
+ final ViewHolder holder = getChildViewHolderInt(child);
+ return holder != null ? holder.getLayoutPosition() : NO_POSITION;
+ }
+
+ /**
+ * Return the stable item id that the given child view corresponds to.
+ *
+ * @param child Child View to query
+ * @return Item id corresponding to the given view or {@link #NO_ID}
+ */
+ public long getChildItemId(View child) {
+ if (mAdapter == null || !mAdapter.hasStableIds()) {
+ return NO_ID;
+ }
+ final ViewHolder holder = getChildViewHolderInt(child);
+ return holder != null ? holder.getItemId() : NO_ID;
+ }
+
+ /**
+ * @deprecated use {@link #findViewHolderForLayoutPosition(int)} or
+ * {@link #findViewHolderForAdapterPosition(int)}
+ */
+ @Deprecated
+ public ViewHolder findViewHolderForPosition(int position) {
+ return findViewHolderForPosition(position, false);
+ }
+
+ /**
+ * Return the ViewHolder for the item in the given position of the data set as of the latest
+ * layout pass.
+ * <p>
+ * This method checks only the children of RecyclerView. If the item at the given
+ * <code>position</code> is not laid out, it <em>will not</em> create a new one.
+ * <p>
+ * Note that when Adapter contents change, ViewHolder positions are not updated until the
+ * next layout calculation. If there are pending adapter updates, the return value of this
+ * method may not match your adapter contents. You can use
+ * #{@link ViewHolder#getAdapterPosition()} to get the current adapter position of a ViewHolder.
+ * <p>
+ * When the ItemAnimator is running a change animation, there might be 2 ViewHolders
+ * with the same layout position representing the same Item. In this case, the updated
+ * ViewHolder will be returned.
+ *
+ * @param position The position of the item in the data set of the adapter
+ * @return The ViewHolder at <code>position</code> or null if there is no such item
+ */
+ public ViewHolder findViewHolderForLayoutPosition(int position) {
+ return findViewHolderForPosition(position, false);
+ }
+
+ /**
+ * Return the ViewHolder for the item in the given position of the data set. Unlike
+ * {@link #findViewHolderForLayoutPosition(int)} this method takes into account any pending
+ * adapter changes that may not be reflected to the layout yet. On the other hand, if
+ * {@link Adapter#notifyDataSetChanged()} has been called but the new layout has not been
+ * calculated yet, this method will return <code>null</code> since the new positions of views
+ * are unknown until the layout is calculated.
+ * <p>
+ * This method checks only the children of RecyclerView. If the item at the given
+ * <code>position</code> is not laid out, it <em>will not</em> create a new one.
+ * <p>
+ * When the ItemAnimator is running a change animation, there might be 2 ViewHolders
+ * representing the same Item. In this case, the updated ViewHolder will be returned.
+ *
+ * @param position The position of the item in the data set of the adapter
+ * @return The ViewHolder at <code>position</code> or null if there is no such item
+ */
+ public ViewHolder findViewHolderForAdapterPosition(int position) {
+ if (mDataSetHasChangedAfterLayout) {
+ return null;
+ }
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ // hidden VHs are not preferred but if that is the only one we find, we rather return it
+ ViewHolder hidden = null;
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.isRemoved()
+ && getAdapterPositionFor(holder) == position) {
+ if (mChildHelper.isHidden(holder.itemView)) {
+ hidden = holder;
+ } else {
+ return holder;
+ }
+ }
+ }
+ return hidden;
+ }
+
+ ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ ViewHolder hidden = null;
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.isRemoved()) {
+ if (checkNewPosition) {
+ if (holder.mPosition != position) {
+ continue;
+ }
+ } else if (holder.getLayoutPosition() != position) {
+ continue;
+ }
+ if (mChildHelper.isHidden(holder.itemView)) {
+ hidden = holder;
+ } else {
+ return holder;
+ }
+ }
+ }
+ // This method should not query cached views. It creates a problem during adapter updates
+ // when we are dealing with already laid out views. Also, for the public method, it is more
+ // reasonable to return null if position is not laid out.
+ return hidden;
+ }
+
+ /**
+ * Return the ViewHolder for the item with the given id. The RecyclerView must
+ * use an Adapter with {@link Adapter#setHasStableIds(boolean) stableIds} to
+ * return a non-null value.
+ * <p>
+ * This method checks only the children of RecyclerView. If the item with the given
+ * <code>id</code> is not laid out, it <em>will not</em> create a new one.
+ *
+ * When the ItemAnimator is running a change animation, there might be 2 ViewHolders with the
+ * same id. In this case, the updated ViewHolder will be returned.
+ *
+ * @param id The id for the requested item
+ * @return The ViewHolder with the given <code>id</code> or null if there is no such item
+ */
+ public ViewHolder findViewHolderForItemId(long id) {
+ if (mAdapter == null || !mAdapter.hasStableIds()) {
+ return null;
+ }
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ ViewHolder hidden = null;
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.isRemoved() && holder.getItemId() == id) {
+ if (mChildHelper.isHidden(holder.itemView)) {
+ hidden = holder;
+ } else {
+ return holder;
+ }
+ }
+ }
+ return hidden;
+ }
+
+ /**
+ * Find the topmost view under the given point.
+ *
+ * @param x Horizontal position in pixels to search
+ * @param y Vertical position in pixels to search
+ * @return The child view under (x, y) or null if no matching child is found
+ */
+ public View findChildViewUnder(float x, float y) {
+ final int count = mChildHelper.getChildCount();
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = mChildHelper.getChildAt(i);
+ final float translationX = child.getTranslationX();
+ final float translationY = child.getTranslationY();
+ if (x >= child.getLeft() + translationX
+ && x <= child.getRight() + translationX
+ && y >= child.getTop() + translationY
+ && y <= child.getBottom() + translationY) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ return super.drawChild(canvas, child, drawingTime);
+ }
+
+ /**
+ * Offset the bounds of all child views by <code>dy</code> pixels.
+ * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
+ *
+ * @param dy Vertical pixel offset to apply to the bounds of all child views
+ */
+ public void offsetChildrenVertical(int dy) {
+ final int childCount = mChildHelper.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
+ }
+ }
+
+ /**
+ * Called when an item view is attached to this RecyclerView.
+ *
+ * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
+ * of child views as they become attached. This will be called before a
+ * {@link LayoutManager} measures or lays out the view and is a good time to perform these
+ * changes.</p>
+ *
+ * @param child Child view that is now attached to this RecyclerView and its associated window
+ */
+ public void onChildAttachedToWindow(View child) {
+ }
+
+ /**
+ * Called when an item view is detached from this RecyclerView.
+ *
+ * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
+ * of child views as they become detached. This will be called as a
+ * {@link LayoutManager} fully detaches the child view from the parent and its window.</p>
+ *
+ * @param child Child view that is now detached from this RecyclerView and its associated window
+ */
+ public void onChildDetachedFromWindow(View child) {
+ }
+
+ /**
+ * Offset the bounds of all child views by <code>dx</code> pixels.
+ * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
+ *
+ * @param dx Horizontal pixel offset to apply to the bounds of all child views
+ */
+ public void offsetChildrenHorizontal(int dx) {
+ final int childCount = mChildHelper.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ mChildHelper.getChildAt(i).offsetLeftAndRight(dx);
+ }
+ }
+
+ /**
+ * Returns the bounds of the view including its decoration and margins.
+ *
+ * @param view The view element to check
+ * @param outBounds A rect that will receive the bounds of the element including its
+ * decoration and margins.
+ */
+ public void getDecoratedBoundsWithMargins(View view, Rect outBounds) {
+ getDecoratedBoundsWithMarginsInt(view, outBounds);
+ }
+
+ static void getDecoratedBoundsWithMarginsInt(View view, Rect outBounds) {
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final Rect insets = lp.mDecorInsets;
+ outBounds.set(view.getLeft() - insets.left - lp.leftMargin,
+ view.getTop() - insets.top - lp.topMargin,
+ view.getRight() + insets.right + lp.rightMargin,
+ view.getBottom() + insets.bottom + lp.bottomMargin);
+ }
+
+ Rect getItemDecorInsetsForChild(View child) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.mInsetsDirty) {
+ return lp.mDecorInsets;
+ }
+
+ if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
+ // changed/invalid items should not be updated until they are rebound.
+ return lp.mDecorInsets;
+ }
+ final Rect insets = lp.mDecorInsets;
+ insets.set(0, 0, 0, 0);
+ final int decorCount = mItemDecorations.size();
+ for (int i = 0; i < decorCount; i++) {
+ mTempRect.set(0, 0, 0, 0);
+ mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
+ insets.left += mTempRect.left;
+ insets.top += mTempRect.top;
+ insets.right += mTempRect.right;
+ insets.bottom += mTempRect.bottom;
+ }
+ lp.mInsetsDirty = false;
+ return insets;
+ }
+
+ /**
+ * Called when the scroll position of this RecyclerView changes. Subclasses should use
+ * this method to respond to scrolling within the adapter's data set instead of an explicit
+ * listener.
+ *
+ * <p>This method will always be invoked before listeners. If a subclass needs to perform
+ * any additional upkeep or bookkeeping after scrolling but before listeners run,
+ * this is a good place to do so.</p>
+ *
+ * <p>This differs from {@link View#onScrollChanged(int, int, int, int)} in that it receives
+ * the distance scrolled in either direction within the adapter's data set instead of absolute
+ * scroll coordinates. Since RecyclerView cannot compute the absolute scroll position from
+ * any arbitrary point in the data set, <code>onScrollChanged</code> will always receive
+ * the current {@link View#getScrollX()} and {@link View#getScrollY()} values which
+ * do not correspond to the data set scroll position. However, some subclasses may choose
+ * to use these fields as special offsets.</p>
+ *
+ * @param dx horizontal distance scrolled in pixels
+ * @param dy vertical distance scrolled in pixels
+ */
+ public void onScrolled(int dx, int dy) {
+ // Do nothing
+ }
+
+ void dispatchOnScrolled(int hresult, int vresult) {
+ mDispatchScrollCounter++;
+ // Pass the current scrollX/scrollY values; no actual change in these properties occurred
+ // but some general-purpose code may choose to respond to changes this way.
+ final int scrollX = getScrollX();
+ final int scrollY = getScrollY();
+ onScrollChanged(scrollX, scrollY, scrollX, scrollY);
+
+ // Pass the real deltas to onScrolled, the RecyclerView-specific method.
+ onScrolled(hresult, vresult);
+
+ // Invoke listeners last. Subclassed view methods always handle the event first.
+ // All internal state is consistent by the time listeners are invoked.
+ if (mScrollListener != null) {
+ mScrollListener.onScrolled(this, hresult, vresult);
+ }
+ if (mScrollListeners != null) {
+ for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
+ mScrollListeners.get(i).onScrolled(this, hresult, vresult);
+ }
+ }
+ mDispatchScrollCounter--;
+ }
+
+ /**
+ * Called when the scroll state of this RecyclerView changes. Subclasses should use this
+ * method to respond to state changes instead of an explicit listener.
+ *
+ * <p>This method will always be invoked before listeners, but after the LayoutManager
+ * responds to the scroll state change.</p>
+ *
+ * @param state the new scroll state, one of {@link #SCROLL_STATE_IDLE},
+ * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}
+ */
+ public void onScrollStateChanged(int state) {
+ // Do nothing
+ }
+
+ void dispatchOnScrollStateChanged(int state) {
+ // Let the LayoutManager go first; this allows it to bring any properties into
+ // a consistent state before the RecyclerView subclass responds.
+ if (mLayout != null) {
+ mLayout.onScrollStateChanged(state);
+ }
+
+ // Let the RecyclerView subclass handle this event next; any LayoutManager property
+ // changes will be reflected by this time.
+ onScrollStateChanged(state);
+
+ // Listeners go last. All other internal state is consistent by this point.
+ if (mScrollListener != null) {
+ mScrollListener.onScrollStateChanged(this, state);
+ }
+ if (mScrollListeners != null) {
+ for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
+ mScrollListeners.get(i).onScrollStateChanged(this, state);
+ }
+ }
+ }
+
+ /**
+ * Returns whether there are pending adapter updates which are not yet applied to the layout.
+ * <p>
+ * If this method returns <code>true</code>, it means that what user is currently seeing may not
+ * reflect them adapter contents (depending on what has changed).
+ * You may use this information to defer or cancel some operations.
+ * <p>
+ * This method returns true if RecyclerView has not yet calculated the first layout after it is
+ * attached to the Window or the Adapter has been replaced.
+ *
+ * @return True if there are some adapter updates which are not yet reflected to layout or false
+ * if layout is up to date.
+ */
+ public boolean hasPendingAdapterUpdates() {
+ return !mFirstLayoutComplete || mDataSetHasChangedAfterLayout
+ || mAdapterHelper.hasPendingUpdates();
+ }
+
+ class ViewFlinger implements Runnable {
+ private int mLastFlingX;
+ private int mLastFlingY;
+ private OverScroller mScroller;
+ Interpolator mInterpolator = sQuinticInterpolator;
+
+
+ // When set to true, postOnAnimation callbacks are delayed until the run method completes
+ private boolean mEatRunOnAnimationRequest = false;
+
+ // Tracks if postAnimationCallback should be re-attached when it is done
+ private boolean mReSchedulePostAnimationCallback = false;
+
+ ViewFlinger() {
+ mScroller = new OverScroller(getContext(), sQuinticInterpolator);
+ }
+
+ @Override
+ public void run() {
+ if (mLayout == null) {
+ stop();
+ return; // no layout, cannot scroll.
+ }
+ disableRunOnAnimationRequests();
+ consumePendingUpdateOperations();
+ // keep a local reference so that if it is changed during onAnimation method, it won't
+ // cause unexpected behaviors
+ final OverScroller scroller = mScroller;
+ final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
+ if (scroller.computeScrollOffset()) {
+ final int x = scroller.getCurrX();
+ final int y = scroller.getCurrY();
+ final int dx = x - mLastFlingX;
+ final int dy = y - mLastFlingY;
+ int hresult = 0;
+ int vresult = 0;
+ mLastFlingX = x;
+ mLastFlingY = y;
+ int overscrollX = 0, overscrollY = 0;
+ if (mAdapter != null) {
+ eatRequestLayout();
+ onEnterLayoutOrScroll();
+ Trace.beginSection(TRACE_SCROLL_TAG);
+ if (dx != 0) {
+ hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
+ overscrollX = dx - hresult;
+ }
+ if (dy != 0) {
+ vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
+ overscrollY = dy - vresult;
+ }
+ Trace.endSection();
+ repositionShadowingViews();
+
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
+
+ if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
+ && smoothScroller.isRunning()) {
+ final int adapterSize = mState.getItemCount();
+ if (adapterSize == 0) {
+ smoothScroller.stop();
+ } else if (smoothScroller.getTargetPosition() >= adapterSize) {
+ smoothScroller.setTargetPosition(adapterSize - 1);
+ smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
+ } else {
+ smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
+ }
+ }
+ }
+ if (!mItemDecorations.isEmpty()) {
+ invalidate();
+ }
+ if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+ considerReleasingGlowsOnScroll(dx, dy);
+ }
+ if (overscrollX != 0 || overscrollY != 0) {
+ final int vel = (int) scroller.getCurrVelocity();
+
+ int velX = 0;
+ if (overscrollX != x) {
+ velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
+ }
+
+ int velY = 0;
+ if (overscrollY != y) {
+ velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
+ }
+
+ if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+ absorbGlows(velX, velY);
+ }
+ if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
+ && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
+ scroller.abortAnimation();
+ }
+ }
+ if (hresult != 0 || vresult != 0) {
+ dispatchOnScrolled(hresult, vresult);
+ }
+
+ if (!awakenScrollBars()) {
+ invalidate();
+ }
+
+ final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically()
+ && vresult == dy;
+ final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally()
+ && hresult == dx;
+ final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal
+ || fullyConsumedVertical;
+
+ if (scroller.isFinished() || !fullyConsumedAny) {
+ setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
+ if (ALLOW_THREAD_GAP_WORK) {
+ mPrefetchRegistry.clearPrefetchPositions();
+ }
+ } else {
+ postOnAnimation();
+ if (mGapWorker != null) {
+ mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
+ }
+ }
+ }
+ // call this after the onAnimation is complete not to have inconsistent callbacks etc.
+ if (smoothScroller != null) {
+ if (smoothScroller.isPendingInitialRun()) {
+ smoothScroller.onAnimation(0, 0);
+ }
+ if (!mReSchedulePostAnimationCallback) {
+ smoothScroller.stop(); //stop if it does not trigger any scroll
+ }
+ }
+ enableRunOnAnimationRequests();
+ }
+
+ private void disableRunOnAnimationRequests() {
+ mReSchedulePostAnimationCallback = false;
+ mEatRunOnAnimationRequest = true;
+ }
+
+ private void enableRunOnAnimationRequests() {
+ mEatRunOnAnimationRequest = false;
+ if (mReSchedulePostAnimationCallback) {
+ postOnAnimation();
+ }
+ }
+
+ void postOnAnimation() {
+ if (mEatRunOnAnimationRequest) {
+ mReSchedulePostAnimationCallback = true;
+ } else {
+ removeCallbacks(this);
+ RecyclerView.this.postOnAnimation(this);
+ }
+ }
+
+ public void fling(int velocityX, int velocityY) {
+ setScrollState(SCROLL_STATE_SETTLING);
+ mLastFlingX = mLastFlingY = 0;
+ mScroller.fling(0, 0, velocityX, velocityY,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+ postOnAnimation();
+ }
+
+ public void smoothScrollBy(int dx, int dy) {
+ smoothScrollBy(dx, dy, 0, 0);
+ }
+
+ public void smoothScrollBy(int dx, int dy, int vx, int vy) {
+ smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, vx, vy));
+ }
+
+ private float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ private int computeScrollDuration(int dx, int dy, int vx, int vy) {
+ final int absDx = Math.abs(dx);
+ final int absDy = Math.abs(dy);
+ final boolean horizontal = absDx > absDy;
+ final int velocity = (int) Math.sqrt(vx * vx + vy * vy);
+ final int delta = (int) Math.sqrt(dx * dx + dy * dy);
+ final int containerSize = horizontal ? getWidth() : getHeight();
+ final int halfContainerSize = containerSize / 2;
+ final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize);
+ final float distance = halfContainerSize + halfContainerSize
+ * distanceInfluenceForSnapDuration(distanceRatio);
+
+ final int duration;
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ float absDelta = (float) (horizontal ? absDx : absDy);
+ duration = (int) (((absDelta / containerSize) + 1) * 300);
+ }
+ return Math.min(duration, MAX_SCROLL_DURATION);
+ }
+
+ public void smoothScrollBy(int dx, int dy, int duration) {
+ smoothScrollBy(dx, dy, duration, sQuinticInterpolator);
+ }
+
+ public void smoothScrollBy(int dx, int dy, Interpolator interpolator) {
+ smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, 0, 0),
+ interpolator == null ? sQuinticInterpolator : interpolator);
+ }
+
+ public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
+ if (mInterpolator != interpolator) {
+ mInterpolator = interpolator;
+ mScroller = new OverScroller(getContext(), interpolator);
+ }
+ setScrollState(SCROLL_STATE_SETTLING);
+ mLastFlingX = mLastFlingY = 0;
+ mScroller.startScroll(0, 0, dx, dy, duration);
+ postOnAnimation();
+ }
+
+ public void stop() {
+ removeCallbacks(this);
+ mScroller.abortAnimation();
+ }
+
+ }
+
+ void repositionShadowingViews() {
+ // Fix up shadow views used by change animations
+ int count = mChildHelper.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View view = mChildHelper.getChildAt(i);
+ ViewHolder holder = getChildViewHolder(view);
+ if (holder != null && holder.mShadowingHolder != null) {
+ View shadowingView = holder.mShadowingHolder.itemView;
+ int left = view.getLeft();
+ int top = view.getTop();
+ if (left != shadowingView.getLeft() || top != shadowingView.getTop()) {
+ shadowingView.layout(left, top,
+ left + shadowingView.getWidth(),
+ top + shadowingView.getHeight());
+ }
+ }
+ }
+ }
+
+ private class RecyclerViewDataObserver extends AdapterDataObserver {
+ RecyclerViewDataObserver() {
+ }
+
+ @Override
+ public void onChanged() {
+ assertNotInLayoutOrScroll(null);
+ mState.mStructureChanged = true;
+
+ setDataSetChangedAfterLayout();
+ if (!mAdapterHelper.hasPendingUpdates()) {
+ requestLayout();
+ }
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
+ triggerUpdateProcessor();
+ }
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
+ triggerUpdateProcessor();
+ }
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
+ triggerUpdateProcessor();
+ }
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
+ triggerUpdateProcessor();
+ }
+ }
+
+ void triggerUpdateProcessor() {
+ if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
+ RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
+ } else {
+ mAdapterUpdateDuringMeasure = true;
+ requestLayout();
+ }
+ }
+ }
+
+ /**
+ * RecycledViewPool lets you share Views between multiple RecyclerViews.
+ * <p>
+ * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
+ * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
+ * <p>
+ * RecyclerView automatically creates a pool for itself if you don't provide one.
+ *
+ */
+ public static class RecycledViewPool {
+ private static final int DEFAULT_MAX_SCRAP = 5;
+
+ /**
+ * Tracks both pooled holders, as well as create/bind timing metadata for the given type.
+ *
+ * Note that this tracks running averages of create/bind time across all RecyclerViews
+ * (and, indirectly, Adapters) that use this pool.
+ *
+ * 1) This enables us to track average create and bind times across multiple adapters. Even
+ * though create (and especially bind) may behave differently for different Adapter
+ * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
+ *
+ * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
+ * false for all other views of its type for the same deadline. This prevents items
+ * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
+ */
+ static class ScrapData {
+ @UnsupportedAppUsage
+ ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
+ int mMaxScrap = DEFAULT_MAX_SCRAP;
+ long mCreateRunningAverageNs = 0;
+ long mBindRunningAverageNs = 0;
+ }
+ SparseArray<ScrapData> mScrap = new SparseArray<>();
+
+ private int mAttachCount = 0;
+
+ public void clear() {
+ for (int i = 0; i < mScrap.size(); i++) {
+ ScrapData data = mScrap.valueAt(i);
+ data.mScrapHeap.clear();
+ }
+ }
+
+ public void setMaxRecycledViews(int viewType, int max) {
+ ScrapData scrapData = getScrapDataForType(viewType);
+ scrapData.mMaxScrap = max;
+ final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
+ if (scrapHeap != null) {
+ while (scrapHeap.size() > max) {
+ scrapHeap.remove(scrapHeap.size() - 1);
+ }
+ }
+ }
+
+ /**
+ * Returns the current number of Views held by the RecycledViewPool of the given view type.
+ */
+ public int getRecycledViewCount(int viewType) {
+ return getScrapDataForType(viewType).mScrapHeap.size();
+ }
+
+ public ViewHolder getRecycledView(int viewType) {
+ final ScrapData scrapData = mScrap.get(viewType);
+ if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
+ final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
+ return scrapHeap.remove(scrapHeap.size() - 1);
+ }
+ return null;
+ }
+
+ int size() {
+ int count = 0;
+ for (int i = 0; i < mScrap.size(); i++) {
+ ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap;
+ if (viewHolders != null) {
+ count += viewHolders.size();
+ }
+ }
+ return count;
+ }
+
+ public void putRecycledView(ViewHolder scrap) {
+ final int viewType = scrap.getItemViewType();
+ final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
+ if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
+ return;
+ }
+ if (DEBUG && scrapHeap.contains(scrap)) {
+ throw new IllegalArgumentException("this scrap item already exists");
+ }
+ scrap.resetInternal();
+ scrapHeap.add(scrap);
+ }
+
+ long runningAverage(long oldAverage, long newValue) {
+ if (oldAverage == 0) {
+ return newValue;
+ }
+ return (oldAverage / 4 * 3) + (newValue / 4);
+ }
+
+ void factorInCreateTime(int viewType, long createTimeNs) {
+ ScrapData scrapData = getScrapDataForType(viewType);
+ scrapData.mCreateRunningAverageNs = runningAverage(
+ scrapData.mCreateRunningAverageNs, createTimeNs);
+ }
+
+ void factorInBindTime(int viewType, long bindTimeNs) {
+ ScrapData scrapData = getScrapDataForType(viewType);
+ scrapData.mBindRunningAverageNs = runningAverage(
+ scrapData.mBindRunningAverageNs, bindTimeNs);
+ }
+
+ boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
+ long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
+ return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
+ }
+
+ boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) {
+ long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs;
+ return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
+ }
+
+ void attach(Adapter adapter) {
+ mAttachCount++;
+ }
+
+ void detach() {
+ mAttachCount--;
+ }
+
+
+ /**
+ * Detaches the old adapter and attaches the new one.
+ * <p>
+ * RecycledViewPool will clear its cache if it has only one adapter attached and the new
+ * adapter uses a different ViewHolder than the oldAdapter.
+ *
+ * @param oldAdapter The previous adapter instance. Will be detached.
+ * @param newAdapter The new adapter instance. Will be attached.
+ * @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
+ * ViewHolder and view types.
+ */
+ void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
+ boolean compatibleWithPrevious) {
+ if (oldAdapter != null) {
+ detach();
+ }
+ if (!compatibleWithPrevious && mAttachCount == 0) {
+ clear();
+ }
+ if (newAdapter != null) {
+ attach(newAdapter);
+ }
+ }
+
+ private ScrapData getScrapDataForType(int viewType) {
+ ScrapData scrapData = mScrap.get(viewType);
+ if (scrapData == null) {
+ scrapData = new ScrapData();
+ mScrap.put(viewType, scrapData);
+ }
+ return scrapData;
+ }
+ }
+
+ /**
+ * Utility method for finding an internal RecyclerView, if present
+ */
+ @Nullable
+ static RecyclerView findNestedRecyclerView(@NonNull View view) {
+ if (!(view instanceof ViewGroup)) {
+ return null;
+ }
+ if (view instanceof RecyclerView) {
+ return (RecyclerView) view;
+ }
+ final ViewGroup parent = (ViewGroup) view;
+ final int count = parent.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView descendant = findNestedRecyclerView(child);
+ if (descendant != null) {
+ return descendant;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Utility method for clearing holder's internal RecyclerView, if present
+ */
+ static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) {
+ if (holder.mNestedRecyclerView != null) {
+ View item = holder.mNestedRecyclerView.get();
+ while (item != null) {
+ if (item == holder.itemView) {
+ return; // match found, don't need to clear
+ }
+
+ ViewParent parent = item.getParent();
+ if (parent instanceof View) {
+ item = (View) parent;
+ } else {
+ item = null;
+ }
+ }
+ holder.mNestedRecyclerView = null; // not nested
+ }
+ }
+
+ /**
+ * Time base for deadline-aware work scheduling. Overridable for testing.
+ *
+ * Will return 0 to avoid cost of System.nanoTime where deadline-aware work scheduling
+ * isn't relevant.
+ */
+ long getNanoTime() {
+ if (ALLOW_THREAD_GAP_WORK) {
+ return System.nanoTime();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * A Recycler is responsible for managing scrapped or detached item views for reuse.
+ *
+ * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
+ * that has been marked for removal or reuse.</p>
+ *
+ * <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
+ * an adapter's data set representing the data at a given position or item ID.
+ * If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
+ * If not, the view can be quickly reused by the LayoutManager with no further work.
+ * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
+ * may be repositioned by a LayoutManager without remeasurement.</p>
+ */
+ public final class Recycler {
+ final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
+ ArrayList<ViewHolder> mChangedScrap = null;
+
+ final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
+
+ private final List<ViewHolder>
+ mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
+
+ private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
+ int mViewCacheMax = DEFAULT_CACHE_SIZE;
+
+ RecycledViewPool mRecyclerPool;
+
+ private ViewCacheExtension mViewCacheExtension;
+
+ static final int DEFAULT_CACHE_SIZE = 2;
+
+ /**
+ * Clear scrap views out of this recycler. Detached views contained within a
+ * recycled view pool will remain.
+ */
+ public void clear() {
+ mAttachedScrap.clear();
+ recycleAndClearCachedViews();
+ }
+
+ /**
+ * Set the maximum number of detached, valid views we should retain for later use.
+ *
+ * @param viewCount Number of views to keep before sending views to the shared pool
+ */
+ public void setViewCacheSize(int viewCount) {
+ mRequestedCacheMax = viewCount;
+ updateViewCacheSize();
+ }
+
+ void updateViewCacheSize() {
+ int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
+ mViewCacheMax = mRequestedCacheMax + extraCache;
+
+ // first, try the views that can be recycled
+ for (int i = mCachedViews.size() - 1;
+ i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
+ recycleCachedViewAt(i);
+ }
+ }
+
+ /**
+ * Returns an unmodifiable list of ViewHolders that are currently in the scrap list.
+ *
+ * @return List of ViewHolders in the scrap list.
+ */
+ public List<ViewHolder> getScrapList() {
+ return mUnmodifiableAttachedScrap;
+ }
+
+ /**
+ * Helper method for getViewForPosition.
+ * <p>
+ * Checks whether a given view holder can be used for the provided position.
+ *
+ * @param holder ViewHolder
+ * @return true if ViewHolder matches the provided position, false otherwise
+ */
+ boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
+ // if it is a removed holder, nothing to verify since we cannot ask adapter anymore
+ // if it is not removed, verify the type and id.
+ if (holder.isRemoved()) {
+ if (DEBUG && !mState.isPreLayout()) {
+ throw new IllegalStateException("should not receive a removed view unless it"
+ + " is pre layout");
+ }
+ return mState.isPreLayout();
+ }
+ if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
+ throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ + "adapter position" + holder);
+ }
+ if (!mState.isPreLayout()) {
+ // don't check type if it is pre-layout.
+ final int type = mAdapter.getItemViewType(holder.mPosition);
+ if (type != holder.getItemViewType()) {
+ return false;
+ }
+ }
+ if (mAdapter.hasStableIds()) {
+ return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
+ }
+ return true;
+ }
+
+ /**
+ * Attempts to bind view, and account for relevant timing information. If
+ * deadlineNs != FOREVER_NS, this method may fail to bind, and return false.
+ *
+ * @param holder Holder to be bound.
+ * @param offsetPosition Position of item to be bound.
+ * @param position Pre-layout position of item to be bound.
+ * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
+ * complete. If FOREVER_NS is passed, this method will not fail to
+ * bind the holder.
+ * @return
+ */
+ private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
+ int position, long deadlineNs) {
+ holder.mOwnerRecyclerView = RecyclerView.this;
+ final int viewType = holder.getItemViewType();
+ long startBindNs = getNanoTime();
+ if (deadlineNs != FOREVER_NS
+ && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
+ // abort - we have a deadline we can't meet
+ return false;
+ }
+ mAdapter.bindViewHolder(holder, offsetPosition);
+ long endBindNs = getNanoTime();
+ mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
+ attachAccessibilityDelegate(holder.itemView);
+ if (mState.isPreLayout()) {
+ holder.mPreLayoutPosition = position;
+ }
+ return true;
+ }
+
+ /**
+ * Binds the given View to the position. The View can be a View previously retrieved via
+ * {@link #getViewForPosition(int)} or created by
+ * {@link Adapter#onCreateViewHolder(ViewGroup, int)}.
+ * <p>
+ * Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)}
+ * and let the RecyclerView handle caching. This is a helper method for LayoutManager who
+ * wants to handle its own recycling logic.
+ * <p>
+ * Note that, {@link #getViewForPosition(int)} already binds the View to the position so
+ * you don't need to call this method unless you want to bind this View to another position.
+ *
+ * @param view The view to update.
+ * @param position The position of the item to bind to this View.
+ */
+ public void bindViewToPosition(View view, int position) {
+ ViewHolder holder = getChildViewHolderInt(view);
+ if (holder == null) {
+ throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot"
+ + " pass arbitrary views to this method, they should be created by the "
+ + "Adapter");
+ }
+ final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+ if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
+ throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ + "position " + position + "(offset:" + offsetPosition + ")."
+ + "state:" + mState.getItemCount());
+ }
+ tryBindViewHolderByDeadline(holder, offsetPosition, position, FOREVER_NS);
+
+ final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
+ final LayoutParams rvLayoutParams;
+ if (lp == null) {
+ rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
+ holder.itemView.setLayoutParams(rvLayoutParams);
+ } else if (!checkLayoutParams(lp)) {
+ rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
+ holder.itemView.setLayoutParams(rvLayoutParams);
+ } else {
+ rvLayoutParams = (LayoutParams) lp;
+ }
+
+ rvLayoutParams.mInsetsDirty = true;
+ rvLayoutParams.mViewHolder = holder;
+ rvLayoutParams.mPendingInvalidate = holder.itemView.getParent() == null;
+ }
+
+ /**
+ * RecyclerView provides artificial position range (item count) in pre-layout state and
+ * automatically maps these positions to {@link Adapter} positions when
+ * {@link #getViewForPosition(int)} or {@link #bindViewToPosition(View, int)} is called.
+ * <p>
+ * Usually, LayoutManager does not need to worry about this. However, in some cases, your
+ * LayoutManager may need to call some custom component with item positions in which
+ * case you need the actual adapter position instead of the pre layout position. You
+ * can use this method to convert a pre-layout position to adapter (post layout) position.
+ * <p>
+ * Note that if the provided position belongs to a deleted ViewHolder, this method will
+ * return -1.
+ * <p>
+ * Calling this method in post-layout state returns the same value back.
+ *
+ * @param position The pre-layout position to convert. Must be greater or equal to 0 and
+ * less than {@link State#getItemCount()}.
+ */
+ public int convertPreLayoutPositionToPostLayout(int position) {
+ if (position < 0 || position >= mState.getItemCount()) {
+ throw new IndexOutOfBoundsException("invalid position " + position + ". State "
+ + "item count is " + mState.getItemCount());
+ }
+ if (!mState.isPreLayout()) {
+ return position;
+ }
+ return mAdapterHelper.findPositionOffset(position);
+ }
+
+ /**
+ * Obtain a view initialized for the given position.
+ *
+ * This method should be used by {@link LayoutManager} implementations to obtain
+ * views to represent data from an {@link Adapter}.
+ * <p>
+ * The Recycler may reuse a scrap or detached view from a shared pool if one is
+ * available for the correct view type. If the adapter has not indicated that the
+ * data at the given position has changed, the Recycler will attempt to hand back
+ * a scrap view that was previously initialized for that data without rebinding.
+ *
+ * @param position Position to obtain a view for
+ * @return A view representing the data at <code>position</code> from <code>adapter</code>
+ */
+ public View getViewForPosition(int position) {
+ return getViewForPosition(position, false);
+ }
+
+ View getViewForPosition(int position, boolean dryRun) {
+ return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
+ }
+
+ /**
+ * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
+ * cache, the RecycledViewPool, or creating it directly.
+ * <p>
+ * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return
+ * rather than constructing or binding a ViewHolder if it doesn't think it has time.
+ * If a ViewHolder must be constructed and not enough time remains, null is returned. If a
+ * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is
+ * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this.
+ *
+ * @param position Position of ViewHolder to be returned.
+ * @param dryRun True if the ViewHolder should not be removed from scrap/cache/
+ * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
+ * complete. If FOREVER_NS is passed, this method will not fail to
+ * create/bind the holder if needed.
+ *
+ * @return ViewHolder for requested position
+ */
+ @Nullable
+ ViewHolder tryGetViewHolderForPositionByDeadline(int position,
+ boolean dryRun, long deadlineNs) {
+ if (position < 0 || position >= mState.getItemCount()) {
+ throw new IndexOutOfBoundsException("Invalid item position " + position
+ + "(" + position + "). Item count:" + mState.getItemCount());
+ }
+ boolean fromScrapOrHiddenOrCache = false;
+ ViewHolder holder = null;
+ // 0) If there is a changed scrap, try to find from there
+ if (mState.isPreLayout()) {
+ holder = getChangedScrapViewForPosition(position);
+ fromScrapOrHiddenOrCache = holder != null;
+ }
+ // 1) Find by position from scrap/hidden list/cache
+ if (holder == null) {
+ holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
+ if (holder != null) {
+ if (!validateViewHolderForOffsetPosition(holder)) {
+ // recycle holder (and unscrap if relevant) since it can't be used
+ if (!dryRun) {
+ // we would like to recycle this but need to make sure it is not used by
+ // animation logic etc.
+ holder.addFlags(ViewHolder.FLAG_INVALID);
+ if (holder.isScrap()) {
+ removeDetachedView(holder.itemView, false);
+ holder.unScrap();
+ } else if (holder.wasReturnedFromScrap()) {
+ holder.clearReturnedFromScrapFlag();
+ }
+ recycleViewHolderInternal(holder);
+ }
+ holder = null;
+ } else {
+ fromScrapOrHiddenOrCache = true;
+ }
+ }
+ }
+ if (holder == null) {
+ final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+ if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
+ throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ + "position " + position + "(offset:" + offsetPosition + ")."
+ + "state:" + mState.getItemCount());
+ }
+
+ final int type = mAdapter.getItemViewType(offsetPosition);
+ // 2) Find from scrap/cache via stable ids, if exists
+ if (mAdapter.hasStableIds()) {
+ holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
+ type, dryRun);
+ if (holder != null) {
+ // update position
+ holder.mPosition = offsetPosition;
+ fromScrapOrHiddenOrCache = true;
+ }
+ }
+ if (holder == null && mViewCacheExtension != null) {
+ // We are NOT sending the offsetPosition because LayoutManager does not
+ // know it.
+ final View view = mViewCacheExtension
+ .getViewForPositionAndType(this, position, type);
+ if (view != null) {
+ holder = getChildViewHolder(view);
+ if (holder == null) {
+ throw new IllegalArgumentException("getViewForPositionAndType returned"
+ + " a view which does not have a ViewHolder");
+ } else if (holder.shouldIgnore()) {
+ throw new IllegalArgumentException("getViewForPositionAndType returned"
+ + " a view that is ignored. You must call stopIgnoring before"
+ + " returning this view.");
+ }
+ }
+ }
+ if (holder == null) { // fallback to pool
+ if (DEBUG) {
+ Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ + position + ") fetching from shared pool");
+ }
+ holder = getRecycledViewPool().getRecycledView(type);
+ if (holder != null) {
+ holder.resetInternal();
+ if (FORCE_INVALIDATE_DISPLAY_LIST) {
+ invalidateDisplayListInt(holder);
+ }
+ }
+ }
+ if (holder == null) {
+ long start = getNanoTime();
+ if (deadlineNs != FOREVER_NS
+ && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
+ // abort - we have a deadline we can't meet
+ return null;
+ }
+ holder = mAdapter.createViewHolder(RecyclerView.this, type);
+ if (ALLOW_THREAD_GAP_WORK) {
+ // only bother finding nested RV if prefetching
+ RecyclerView innerView = findNestedRecyclerView(holder.itemView);
+ if (innerView != null) {
+ holder.mNestedRecyclerView = new WeakReference<>(innerView);
+ }
+ }
+
+ long end = getNanoTime();
+ mRecyclerPool.factorInCreateTime(type, end - start);
+ if (DEBUG) {
+ Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
+ }
+ }
+ }
+
+ // This is very ugly but the only place we can grab this information
+ // before the View is rebound and returned to the LayoutManager for post layout ops.
+ // We don't need this in pre-layout since the VH is not updated by the LM.
+ if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
+ .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
+ holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ if (mState.mRunSimpleAnimations) {
+ int changeFlags = ItemAnimator
+ .buildAdapterChangeFlagsForAnimations(holder);
+ changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+ final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
+ holder, changeFlags, holder.getUnmodifiedPayloads());
+ recordAnimationInfoIfBouncedHiddenView(holder, info);
+ }
+ }
+
+ boolean bound = false;
+ if (mState.isPreLayout() && holder.isBound()) {
+ // do not update unless we absolutely have to.
+ holder.mPreLayoutPosition = position;
+ } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
+ if (DEBUG && holder.isRemoved()) {
+ throw new IllegalStateException("Removed holder should be bound and it should"
+ + " come here only in pre-layout. Holder: " + holder);
+ }
+ final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+ bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
+ }
+
+ final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
+ final LayoutParams rvLayoutParams;
+ if (lp == null) {
+ rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
+ holder.itemView.setLayoutParams(rvLayoutParams);
+ } else if (!checkLayoutParams(lp)) {
+ rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
+ holder.itemView.setLayoutParams(rvLayoutParams);
+ } else {
+ rvLayoutParams = (LayoutParams) lp;
+ }
+ rvLayoutParams.mViewHolder = holder;
+ rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
+ return holder;
+ }
+
+ private void attachAccessibilityDelegate(View itemView) {
+ if (isAccessibilityEnabled()) {
+ if (itemView.getImportantForAccessibility()
+ == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ if (itemView.getAccessibilityDelegate() == null) {
+ itemView.setAccessibilityDelegate(mAccessibilityDelegate.getItemDelegate());
+ }
+ }
+ }
+
+ private void invalidateDisplayListInt(ViewHolder holder) {
+ if (holder.itemView instanceof ViewGroup) {
+ invalidateDisplayListInt((ViewGroup) holder.itemView, false);
+ }
+ }
+
+ private void invalidateDisplayListInt(ViewGroup viewGroup, boolean invalidateThis) {
+ for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) {
+ final View view = viewGroup.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ invalidateDisplayListInt((ViewGroup) view, true);
+ }
+ }
+ if (!invalidateThis) {
+ return;
+ }
+ // we need to force it to become invisible
+ if (viewGroup.getVisibility() == View.INVISIBLE) {
+ viewGroup.setVisibility(View.VISIBLE);
+ viewGroup.setVisibility(View.INVISIBLE);
+ } else {
+ final int visibility = viewGroup.getVisibility();
+ viewGroup.setVisibility(View.INVISIBLE);
+ viewGroup.setVisibility(visibility);
+ }
+ }
+
+ /**
+ * Recycle a detached view. The specified view will be added to a pool of views
+ * for later rebinding and reuse.
+ *
+ * <p>A view must be fully detached (removed from parent) before it may be recycled. If the
+ * View is scrapped, it will be removed from scrap list.</p>
+ *
+ * @param view Removed view for recycling
+ * @see LayoutManager#removeAndRecycleView(View, Recycler)
+ */
+ public void recycleView(View view) {
+ // This public recycle method tries to make view recycle-able since layout manager
+ // intended to recycle this view (e.g. even if it is in scrap or change cache)
+ ViewHolder holder = getChildViewHolderInt(view);
+ if (holder.isTmpDetached()) {
+ removeDetachedView(view, false);
+ }
+ if (holder.isScrap()) {
+ holder.unScrap();
+ } else if (holder.wasReturnedFromScrap()) {
+ holder.clearReturnedFromScrapFlag();
+ }
+ recycleViewHolderInternal(holder);
+ }
+
+ /**
+ * Internally, use this method instead of {@link #recycleView(android.view.View)} to
+ * catch potential bugs.
+ * @param view
+ */
+ void recycleViewInternal(View view) {
+ recycleViewHolderInternal(getChildViewHolderInt(view));
+ }
+
+ void recycleAndClearCachedViews() {
+ final int count = mCachedViews.size();
+ for (int i = count - 1; i >= 0; i--) {
+ recycleCachedViewAt(i);
+ }
+ mCachedViews.clear();
+ if (ALLOW_THREAD_GAP_WORK) {
+ mPrefetchRegistry.clearPrefetchPositions();
+ }
+ }
+
+ /**
+ * Recycles a cached view and removes the view from the list. Views are added to cache
+ * if and only if they are recyclable, so this method does not check it again.
+ * <p>
+ * A small exception to this rule is when the view does not have an animator reference
+ * but transient state is true (due to animations created outside ItemAnimator). In that
+ * case, adapter may choose to recycle it. From RecyclerView's perspective, the view is
+ * still recyclable since Adapter wants to do so.
+ *
+ * @param cachedViewIndex The index of the view in cached views list
+ */
+ void recycleCachedViewAt(int cachedViewIndex) {
+ if (DEBUG) {
+ Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
+ }
+ ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
+ if (DEBUG) {
+ Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
+ }
+ addViewHolderToRecycledViewPool(viewHolder, true);
+ mCachedViews.remove(cachedViewIndex);
+ }
+
+ /**
+ * internal implementation checks if view is scrapped or attached and throws an exception
+ * if so.
+ * Public version un-scraps before calling recycle.
+ */
+ void recycleViewHolderInternal(ViewHolder holder) {
+ if (holder.isScrap() || holder.itemView.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Scrapped or attached views may not be recycled. isScrap:"
+ + holder.isScrap() + " isAttached:"
+ + (holder.itemView.getParent() != null));
+ }
+
+ if (holder.isTmpDetached()) {
+ throw new IllegalArgumentException("Tmp detached view should be removed "
+ + "from RecyclerView before it can be recycled: " + holder);
+ }
+
+ if (holder.shouldIgnore()) {
+ throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+ + " should first call stopIgnoringView(view) before calling recycle.");
+ }
+ //noinspection unchecked
+ final boolean transientStatePreventsRecycling = holder
+ .doesTransientStatePreventRecycling();
+ final boolean forceRecycle = mAdapter != null
+ && transientStatePreventsRecycling
+ && mAdapter.onFailedToRecycleView(holder);
+ boolean cached = false;
+ boolean recycled = false;
+ if (DEBUG && mCachedViews.contains(holder)) {
+ throw new IllegalArgumentException("cached view received recycle internal? "
+ + holder);
+ }
+ if (forceRecycle || holder.isRecyclable()) {
+ if (mViewCacheMax > 0
+ && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
+ | ViewHolder.FLAG_REMOVED
+ | ViewHolder.FLAG_UPDATE
+ | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
+ // Retire oldest cached view
+ int cachedViewSize = mCachedViews.size();
+ if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
+ recycleCachedViewAt(0);
+ cachedViewSize--;
+ }
+
+ int targetCacheIndex = cachedViewSize;
+ if (ALLOW_THREAD_GAP_WORK
+ && cachedViewSize > 0
+ && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
+ // when adding the view, skip past most recently prefetched views
+ int cacheIndex = cachedViewSize - 1;
+ while (cacheIndex >= 0) {
+ int cachedPos = mCachedViews.get(cacheIndex).mPosition;
+ if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
+ break;
+ }
+ cacheIndex--;
+ }
+ targetCacheIndex = cacheIndex + 1;
+ }
+ mCachedViews.add(targetCacheIndex, holder);
+ cached = true;
+ }
+ if (!cached) {
+ addViewHolderToRecycledViewPool(holder, true);
+ recycled = true;
+ }
+ } else {
+ // NOTE: A view can fail to be recycled when it is scrolled off while an animation
+ // runs. In this case, the item is eventually recycled by
+ // ItemAnimatorRestoreListener#onAnimationFinished.
+
+ // TODO: consider cancelling an animation when an item is removed scrollBy,
+ // to return it to the pool faster
+ if (DEBUG) {
+ Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ + "re-visit here. We are still removing it from animation lists");
+ }
+ }
+ // even if the holder is not removed, we still call this method so that it is removed
+ // from view holder lists.
+ mViewInfoStore.removeViewHolder(holder);
+ if (!cached && !recycled && transientStatePreventsRecycling) {
+ holder.mOwnerRecyclerView = null;
+ }
+ }
+
+ /**
+ * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool.
+ *
+ * Pass false to dispatchRecycled for views that have not been bound.
+ *
+ * @param holder Holder to be added to the pool.
+ * @param dispatchRecycled True to dispatch View recycled callbacks.
+ */
+ void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
+ clearNestedRecyclerViewIfNotNested(holder);
+ holder.itemView.setAccessibilityDelegate(null);
+ if (dispatchRecycled) {
+ dispatchViewRecycled(holder);
+ }
+ holder.mOwnerRecyclerView = null;
+ getRecycledViewPool().putRecycledView(holder);
+ }
+
+ /**
+ * Used as a fast path for unscrapping and recycling a view during a bulk operation.
+ * The caller must call {@link #clearScrap()} when it's done to update the recycler's
+ * internal bookkeeping.
+ */
+ void quickRecycleScrapView(View view) {
+ final ViewHolder holder = getChildViewHolderInt(view);
+ holder.mScrapContainer = null;
+ holder.mInChangeScrap = false;
+ holder.clearReturnedFromScrapFlag();
+ recycleViewHolderInternal(holder);
+ }
+
+ /**
+ * Mark an attached view as scrap.
+ *
+ * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
+ * for rebinding and reuse. Requests for a view for a given position may return a
+ * reused or rebound scrap view instance.</p>
+ *
+ * @param view View to scrap
+ */
+ void scrapView(View view) {
+ final ViewHolder holder = getChildViewHolderInt(view);
+ if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
+ || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
+ if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
+ throw new IllegalArgumentException("Called scrap view with an invalid view."
+ + " Invalid views cannot be reused from scrap, they should rebound from"
+ + " recycler pool.");
+ }
+ holder.setScrapContainer(this, false);
+ mAttachedScrap.add(holder);
+ } else {
+ if (mChangedScrap == null) {
+ mChangedScrap = new ArrayList<ViewHolder>();
+ }
+ holder.setScrapContainer(this, true);
+ mChangedScrap.add(holder);
+ }
+ }
+
+ /**
+ * Remove a previously scrapped view from the pool of eligible scrap.
+ *
+ * <p>This view will no longer be eligible for reuse until re-scrapped or
+ * until it is explicitly removed and recycled.</p>
+ */
+ void unscrapView(ViewHolder holder) {
+ if (holder.mInChangeScrap) {
+ mChangedScrap.remove(holder);
+ } else {
+ mAttachedScrap.remove(holder);
+ }
+ holder.mScrapContainer = null;
+ holder.mInChangeScrap = false;
+ holder.clearReturnedFromScrapFlag();
+ }
+
+ int getScrapCount() {
+ return mAttachedScrap.size();
+ }
+
+ View getScrapViewAt(int index) {
+ return mAttachedScrap.get(index).itemView;
+ }
+
+ void clearScrap() {
+ mAttachedScrap.clear();
+ if (mChangedScrap != null) {
+ mChangedScrap.clear();
+ }
+ }
+
+ ViewHolder getChangedScrapViewForPosition(int position) {
+ // If pre-layout, check the changed scrap for an exact match.
+ final int changedScrapSize;
+ if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
+ return null;
+ }
+ // find by position
+ for (int i = 0; i < changedScrapSize; i++) {
+ final ViewHolder holder = mChangedScrap.get(i);
+ if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
+ holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+ return holder;
+ }
+ }
+ // find by id
+ if (mAdapter.hasStableIds()) {
+ final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+ if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
+ final long id = mAdapter.getItemId(offsetPosition);
+ for (int i = 0; i < changedScrapSize; i++) {
+ final ViewHolder holder = mChangedScrap.get(i);
+ if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
+ holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+ return holder;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a view for the position either from attach scrap, hidden children, or cache.
+ *
+ * @param position Item position
+ * @param dryRun Does a dry run, finds the ViewHolder but does not remove
+ * @return a ViewHolder that can be re-used for this position.
+ */
+ ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
+ final int scrapCount = mAttachedScrap.size();
+
+ // Try first for an exact, non-invalid match from scrap.
+ for (int i = 0; i < scrapCount; i++) {
+ final ViewHolder holder = mAttachedScrap.get(i);
+ if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
+ && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
+ holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+ return holder;
+ }
+ }
+
+ if (!dryRun) {
+ View view = mChildHelper.findHiddenNonRemovedView(position);
+ if (view != null) {
+ // This View is good to be used. We just need to unhide, detach and move to the
+ // scrap list.
+ final ViewHolder vh = getChildViewHolderInt(view);
+ mChildHelper.unhide(view);
+ int layoutIndex = mChildHelper.indexOfChild(view);
+ if (layoutIndex == RecyclerView.NO_POSITION) {
+ throw new IllegalStateException("layout index should not be -1 after "
+ + "unhiding a view:" + vh);
+ }
+ mChildHelper.detachViewFromParent(layoutIndex);
+ scrapView(view);
+ vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
+ | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ return vh;
+ }
+ }
+
+ // Search in our first-level recycled view cache.
+ final int cacheSize = mCachedViews.size();
+ for (int i = 0; i < cacheSize; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ // invalid view holders may be in cache if adapter has stable ids as they can be
+ // retrieved via getScrapOrCachedViewForId
+ if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
+ if (!dryRun) {
+ mCachedViews.remove(i);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ + ") found match in cache: " + holder);
+ }
+ return holder;
+ }
+ }
+ return null;
+ }
+
+ ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
+ // Look in our attached views first
+ final int count = mAttachedScrap.size();
+ for (int i = count - 1; i >= 0; i--) {
+ final ViewHolder holder = mAttachedScrap.get(i);
+ if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
+ if (type == holder.getItemViewType()) {
+ holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+ if (holder.isRemoved()) {
+ // this might be valid in two cases:
+ // > item is removed but we are in pre-layout pass
+ // >> do nothing. return as is. make sure we don't rebind
+ // > item is removed then added to another position and we are in
+ // post layout.
+ // >> remove removed and invalid flags, add update flag to rebind
+ // because item was invisible to us and we don't know what happened in
+ // between.
+ if (!mState.isPreLayout()) {
+ holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
+ | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
+ }
+ }
+ return holder;
+ } else if (!dryRun) {
+ // if we are running animations, it is actually better to keep it in scrap
+ // but this would force layout manager to lay it out which would be bad.
+ // Recycle this scrap. Type mismatch.
+ mAttachedScrap.remove(i);
+ removeDetachedView(holder.itemView, false);
+ quickRecycleScrapView(holder.itemView);
+ }
+ }
+ }
+
+ // Search the first-level cache
+ final int cacheSize = mCachedViews.size();
+ for (int i = cacheSize - 1; i >= 0; i--) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder.getItemId() == id) {
+ if (type == holder.getItemViewType()) {
+ if (!dryRun) {
+ mCachedViews.remove(i);
+ }
+ return holder;
+ } else if (!dryRun) {
+ recycleCachedViewAt(i);
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+
+ void dispatchViewRecycled(ViewHolder holder) {
+ if (mRecyclerListener != null) {
+ mRecyclerListener.onViewRecycled(holder);
+ }
+ if (mAdapter != null) {
+ mAdapter.onViewRecycled(holder);
+ }
+ if (mState != null) {
+ mViewInfoStore.removeViewHolder(holder);
+ }
+ if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
+ }
+
+ void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
+ boolean compatibleWithPrevious) {
+ clear();
+ getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);
+ }
+
+ void offsetPositionRecordsForMove(int from, int to) {
+ final int start, end, inBetweenOffset;
+ if (from < to) {
+ start = from;
+ end = to;
+ inBetweenOffset = -1;
+ } else {
+ start = to;
+ end = from;
+ inBetweenOffset = 1;
+ }
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder == null || holder.mPosition < start || holder.mPosition > end) {
+ continue;
+ }
+ if (holder.mPosition == from) {
+ holder.offsetPosition(to - from, false);
+ } else {
+ holder.offsetPosition(inBetweenOffset, false);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder "
+ + holder);
+ }
+ }
+ }
+
+ void offsetPositionRecordsForInsert(int insertedAt, int count) {
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder != null && holder.mPosition >= insertedAt) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder "
+ + holder + " now at position " + (holder.mPosition + count));
+ }
+ holder.offsetPosition(count, true);
+ }
+ }
+ }
+
+ /**
+ * @param removedFrom Remove start index
+ * @param count Remove count
+ * @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if
+ * false, they'll be applied before the second layout pass
+ */
+ void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) {
+ final int removedEnd = removedFrom + count;
+ final int cachedCount = mCachedViews.size();
+ for (int i = cachedCount - 1; i >= 0; i--) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder != null) {
+ if (holder.mPosition >= removedEnd) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove cached " + i
+ + " holder " + holder + " now at position "
+ + (holder.mPosition - count));
+ }
+ holder.offsetPosition(-count, applyToPreLayout);
+ } else if (holder.mPosition >= removedFrom) {
+ // Item for this view was removed. Dump it from the cache.
+ holder.addFlags(ViewHolder.FLAG_REMOVED);
+ recycleCachedViewAt(i);
+ }
+ }
+ }
+ }
+
+ void setViewCacheExtension(ViewCacheExtension extension) {
+ mViewCacheExtension = extension;
+ }
+
+ void setRecycledViewPool(RecycledViewPool pool) {
+ if (mRecyclerPool != null) {
+ mRecyclerPool.detach();
+ }
+ mRecyclerPool = pool;
+ if (pool != null) {
+ mRecyclerPool.attach(getAdapter());
+ }
+ }
+
+ RecycledViewPool getRecycledViewPool() {
+ if (mRecyclerPool == null) {
+ mRecyclerPool = new RecycledViewPool();
+ }
+ return mRecyclerPool;
+ }
+
+ void viewRangeUpdate(int positionStart, int itemCount) {
+ final int positionEnd = positionStart + itemCount;
+ final int cachedCount = mCachedViews.size();
+ for (int i = cachedCount - 1; i >= 0; i--) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder == null) {
+ continue;
+ }
+
+ final int pos = holder.getLayoutPosition();
+ if (pos >= positionStart && pos < positionEnd) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE);
+ recycleCachedViewAt(i);
+ // cached views should not be flagged as changed because this will cause them
+ // to animate when they are returned from cache.
+ }
+ }
+ }
+
+ void setAdapterPositionsAsUnknown() {
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder != null) {
+ holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
+ }
+ }
+ }
+
+ void markKnownViewsInvalid() {
+ if (mAdapter != null && mAdapter.hasStableIds()) {
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder != null) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ holder.addChangePayload(null);
+ }
+ }
+ } else {
+ // we cannot re-use cached views in this case. Recycle them all
+ recycleAndClearCachedViews();
+ }
+ }
+
+ void clearOldPositions() {
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ holder.clearOldPosition();
+ }
+ final int scrapCount = mAttachedScrap.size();
+ for (int i = 0; i < scrapCount; i++) {
+ mAttachedScrap.get(i).clearOldPosition();
+ }
+ if (mChangedScrap != null) {
+ final int changedScrapCount = mChangedScrap.size();
+ for (int i = 0; i < changedScrapCount; i++) {
+ mChangedScrap.get(i).clearOldPosition();
+ }
+ }
+ }
+
+ void markItemDecorInsetsDirty() {
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
+ if (layoutParams != null) {
+ layoutParams.mInsetsDirty = true;
+ }
+ }
+ }
+ }
+
+ /**
+ * ViewCacheExtension is a helper class to provide an additional layer of view caching that can
+ * be controlled by the developer.
+ * <p>
+ * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
+ * first level cache to find a matching View. If it cannot find a suitable View, Recycler will
+ * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
+ * {@link RecycledViewPool}.
+ * <p>
+ * Note that, Recycler never sends Views to this method to be cached. It is developers
+ * responsibility to decide whether they want to keep their Views in this custom cache or let
+ * the default recycling policy handle it.
+ */
+ public abstract static class ViewCacheExtension {
+
+ /**
+ * Returns a View that can be binded to the given Adapter position.
+ * <p>
+ * This method should <b>not</b> create a new View. Instead, it is expected to return
+ * an already created View that can be re-used for the given type and position.
+ * If the View is marked as ignored, it should first call
+ * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
+ * <p>
+ * RecyclerView will re-bind the returned View to the position if necessary.
+ *
+ * @param recycler The Recycler that can be used to bind the View
+ * @param position The adapter position
+ * @param type The type of the View, defined by adapter
+ * @return A View that is bound to the given position or NULL if there is no View to re-use
+ * @see LayoutManager#ignoreView(View)
+ */
+ public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
+ }
+
+ /**
+ * Base class for an Adapter
+ *
+ * <p>Adapters provide a binding from an app-specific data set to views that are displayed
+ * within a {@link RecyclerView}.</p>
+ *
+ * @param <VH> A class that extends ViewHolder that will be used by the adapter.
+ */
+ public abstract static class Adapter<VH extends ViewHolder> {
+ private final AdapterDataObservable mObservable = new AdapterDataObservable();
+ private boolean mHasStableIds = false;
+
+ /**
+ * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent
+ * an item.
+ * <p>
+ * This new ViewHolder should be constructed with a new View that can represent the items
+ * of the given type. You can either create a new View manually or inflate it from an XML
+ * layout file.
+ * <p>
+ * The new ViewHolder will be used to display items of the adapter using
+ * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display
+ * different items in the data set, it is a good idea to cache references to sub views of
+ * the View to avoid unnecessary {@link View#findViewById(int)} calls.
+ *
+ * @param parent The ViewGroup into which the new View will be added after it is bound to
+ * an adapter position.
+ * @param viewType The view type of the new View.
+ *
+ * @return A new ViewHolder that holds a View of the given view type.
+ * @see #getItemViewType(int)
+ * @see #onBindViewHolder(ViewHolder, int)
+ */
+ public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
+
+ /**
+ * Called by RecyclerView to display the data at the specified position. This method should
+ * update the contents of the {@link ViewHolder#itemView} to reflect the item at the given
+ * position.
+ * <p>
+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
+ * again if the position of the item changes in the data set unless the item itself is
+ * invalidated or the new position cannot be determined. For this reason, you should only
+ * use the <code>position</code> parameter while acquiring the related data item inside
+ * this method and should not keep a copy of it. If you need the position of an item later
+ * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
+ * have the updated adapter position.
+ *
+ * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can
+ * handle efficient partial bind.
+ *
+ * @param holder The ViewHolder which should be updated to represent the contents of the
+ * item at the given position in the data set.
+ * @param position The position of the item within the adapter's data set.
+ */
+ public abstract void onBindViewHolder(VH holder, int position);
+
+ /**
+ * Called by RecyclerView to display the data at the specified position. This method
+ * should update the contents of the {@link ViewHolder#itemView} to reflect the item at
+ * the given position.
+ * <p>
+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
+ * again if the position of the item changes in the data set unless the item itself is
+ * invalidated or the new position cannot be determined. For this reason, you should only
+ * use the <code>position</code> parameter while acquiring the related data item inside
+ * this method and should not keep a copy of it. If you need the position of an item later
+ * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
+ * have the updated adapter position.
+ * <p>
+ * Partial bind vs full bind:
+ * <p>
+ * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
+ * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty,
+ * the ViewHolder is currently bound to old data and Adapter may run an efficient partial
+ * update using the payload info. If the payload is empty, Adapter must run a full bind.
+ * Adapter should not assume that the payload passed in notify methods will be received by
+ * onBindViewHolder(). For example when the view is not attached to the screen, the
+ * payload in notifyItemChange() will be simply dropped.
+ *
+ * @param holder The ViewHolder which should be updated to represent the contents of the
+ * item at the given position in the data set.
+ * @param position The position of the item within the adapter's data set.
+ * @param payloads A non-null list of merged payloads. Can be empty list if requires full
+ * update.
+ */
+ public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
+ onBindViewHolder(holder, position);
+ }
+
+ /**
+ * This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new
+ * {@link ViewHolder} and initializes some private fields to be used by RecyclerView.
+ *
+ * @see #onCreateViewHolder(ViewGroup, int)
+ */
+ public final VH createViewHolder(ViewGroup parent, int viewType) {
+ Trace.beginSection(TRACE_CREATE_VIEW_TAG);
+ final VH holder = onCreateViewHolder(parent, viewType);
+ holder.mItemViewType = viewType;
+ Trace.endSection();
+ return holder;
+ }
+
+ /**
+ * This method internally calls {@link #onBindViewHolder(ViewHolder, int)} to update the
+ * {@link ViewHolder} contents with the item at the given position and also sets up some
+ * private fields to be used by RecyclerView.
+ *
+ * @see #onBindViewHolder(ViewHolder, int)
+ */
+ public final void bindViewHolder(VH holder, int position) {
+ holder.mPosition = position;
+ if (hasStableIds()) {
+ holder.mItemId = getItemId(position);
+ }
+ holder.setFlags(ViewHolder.FLAG_BOUND,
+ ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
+ | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
+ Trace.beginSection(TRACE_BIND_VIEW_TAG);
+ onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
+ holder.clearPayload();
+ final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
+ if (layoutParams instanceof RecyclerView.LayoutParams) {
+ ((LayoutParams) layoutParams).mInsetsDirty = true;
+ }
+ Trace.endSection();
+ }
+
+ /**
+ * Return the view type of the item at <code>position</code> for the purposes
+ * of view recycling.
+ *
+ * <p>The default implementation of this method returns 0, making the assumption of
+ * a single view type for the adapter. Unlike ListView adapters, types need not
+ * be contiguous. Consider using id resources to uniquely identify item view types.
+ *
+ * @param position position to query
+ * @return integer value identifying the type of the view needed to represent the item at
+ * <code>position</code>. Type codes need not be contiguous.
+ */
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ /**
+ * Indicates whether each item in the data set can be represented with a unique identifier
+ * of type {@link java.lang.Long}.
+ *
+ * @param hasStableIds Whether items in data set have unique identifiers or not.
+ * @see #hasStableIds()
+ * @see #getItemId(int)
+ */
+ public void setHasStableIds(boolean hasStableIds) {
+ if (hasObservers()) {
+ throw new IllegalStateException("Cannot change whether this adapter has "
+ + "stable IDs while the adapter has registered observers.");
+ }
+ mHasStableIds = hasStableIds;
+ }
+
+ /**
+ * Return the stable ID for the item at <code>position</code>. If {@link #hasStableIds()}
+ * would return false this method should return {@link #NO_ID}. The default implementation
+ * of this method returns {@link #NO_ID}.
+ *
+ * @param position Adapter position to query
+ * @return the stable ID of the item at position
+ */
+ public long getItemId(int position) {
+ return NO_ID;
+ }
+
+ /**
+ * Returns the total number of items in the data set held by the adapter.
+ *
+ * @return The total number of items in this adapter.
+ */
+ public abstract int getItemCount();
+
+ /**
+ * Returns true if this adapter publishes a unique <code>long</code> value that can
+ * act as a key for the item at a given position in the data set. If that item is relocated
+ * in the data set, the ID returned for that item should be the same.
+ *
+ * @return true if this adapter's items have stable IDs
+ */
+ public final boolean hasStableIds() {
+ return mHasStableIds;
+ }
+
+ /**
+ * Called when a view created by this adapter has been recycled.
+ *
+ * <p>A view is recycled when a {@link LayoutManager} decides that it no longer
+ * needs to be attached to its parent {@link RecyclerView}. This can be because it has
+ * fallen out of visibility or a set of cached views represented by views still
+ * attached to the parent RecyclerView. If an item view has large or expensive data
+ * bound to it such as large bitmaps, this may be a good place to release those
+ * resources.</p>
+ * <p>
+ * RecyclerView calls this method right before clearing ViewHolder's internal data and
+ * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information
+ * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get
+ * its adapter position.
+ *
+ * @param holder The ViewHolder for the view being recycled
+ */
+ public void onViewRecycled(VH holder) {
+ }
+
+ /**
+ * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled
+ * due to its transient state. Upon receiving this callback, Adapter can clear the
+ * animation(s) that effect the View's transient state and return <code>true</code> so that
+ * the View can be recycled. Keep in mind that the View in question is already removed from
+ * the RecyclerView.
+ * <p>
+ * In some cases, it is acceptable to recycle a View although it has transient state. Most
+ * of the time, this is a case where the transient state will be cleared in
+ * {@link #onBindViewHolder(ViewHolder, int)} call when View is rebound to a new position.
+ * For this reason, RecyclerView leaves the decision to the Adapter and uses the return
+ * value of this method to decide whether the View should be recycled or not.
+ * <p>
+ * Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you
+ * should never receive this callback because RecyclerView keeps those Views as children
+ * until their animations are complete. This callback is useful when children of the item
+ * views create animations which may not be easy to implement using an {@link ItemAnimator}.
+ * <p>
+ * You should <em>never</em> fix this issue by calling
+ * <code>holder.itemView.setHasTransientState(false);</code> unless you've previously called
+ * <code>holder.itemView.setHasTransientState(true);</code>. Each
+ * <code>View.setHasTransientState(true)</code> call must be matched by a
+ * <code>View.setHasTransientState(false)</code> call, otherwise, the state of the View
+ * may become inconsistent. You should always prefer to end or cancel animations that are
+ * triggering the transient state instead of handling it manually.
+ *
+ * @param holder The ViewHolder containing the View that could not be recycled due to its
+ * transient state.
+ * @return True if the View should be recycled, false otherwise. Note that if this method
+ * returns <code>true</code>, RecyclerView <em>will ignore</em> the transient state of
+ * the View and recycle it regardless. If this method returns <code>false</code>,
+ * RecyclerView will check the View's transient state again before giving a final decision.
+ * Default implementation returns false.
+ */
+ public boolean onFailedToRecycleView(VH holder) {
+ return false;
+ }
+
+ /**
+ * Called when a view created by this adapter has been attached to a window.
+ *
+ * <p>This can be used as a reasonable signal that the view is about to be seen
+ * by the user. If the adapter previously freed any resources in
+ * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow}
+ * those resources should be restored here.</p>
+ *
+ * @param holder Holder of the view being attached
+ */
+ public void onViewAttachedToWindow(VH holder) {
+ }
+
+ /**
+ * Called when a view created by this adapter has been detached from its window.
+ *
+ * <p>Becoming detached from the window is not necessarily a permanent condition;
+ * the consumer of an Adapter's views may choose to cache views offscreen while they
+ * are not visible, attaching and detaching them as appropriate.</p>
+ *
+ * @param holder Holder of the view being detached
+ */
+ public void onViewDetachedFromWindow(VH holder) {
+ }
+
+ /**
+ * Returns true if one or more observers are attached to this adapter.
+ *
+ * @return true if this adapter has observers
+ */
+ public final boolean hasObservers() {
+ return mObservable.hasObservers();
+ }
+
+ /**
+ * Register a new observer to listen for data changes.
+ *
+ * <p>The adapter may publish a variety of events describing specific changes.
+ * Not all adapters may support all change types and some may fall back to a generic
+ * {@link com.android.internal.widget.RecyclerView.AdapterDataObserver#onChanged()
+ * "something changed"} event if more specific data is not available.</p>
+ *
+ * <p>Components registering observers with an adapter are responsible for
+ * {@link #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver)
+ * unregistering} those observers when finished.</p>
+ *
+ * @param observer Observer to register
+ *
+ * @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver)
+ */
+ public void registerAdapterDataObserver(AdapterDataObserver observer) {
+ mObservable.registerObserver(observer);
+ }
+
+ /**
+ * Unregister an observer currently listening for data changes.
+ *
+ * <p>The unregistered observer will no longer receive events about changes
+ * to the adapter.</p>
+ *
+ * @param observer Observer to unregister
+ *
+ * @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver)
+ */
+ public void unregisterAdapterDataObserver(AdapterDataObserver observer) {
+ mObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * Called by RecyclerView when it starts observing this Adapter.
+ * <p>
+ * Keep in mind that same adapter may be observed by multiple RecyclerViews.
+ *
+ * @param recyclerView The RecyclerView instance which started observing this adapter.
+ * @see #onDetachedFromRecyclerView(RecyclerView)
+ */
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ }
+
+ /**
+ * Called by RecyclerView when it stops observing this Adapter.
+ *
+ * @param recyclerView The RecyclerView instance which stopped observing this adapter.
+ * @see #onAttachedToRecyclerView(RecyclerView)
+ */
+ public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+ }
+
+ /**
+ * Notify any registered observers that the data set has changed.
+ *
+ * <p>There are two different classes of data change events, item changes and structural
+ * changes. Item changes are when a single item has its data updated but no positional
+ * changes have occurred. Structural changes are when items are inserted, removed or moved
+ * within the data set.</p>
+ *
+ * <p>This event does not specify what about the data set has changed, forcing
+ * any observers to assume that all existing items and structure may no longer be valid.
+ * LayoutManagers will be forced to fully rebind and relayout all visible views.</p>
+ *
+ * <p><code>RecyclerView</code> will attempt to synthesize visible structural change events
+ * for adapters that report that they have {@link #hasStableIds() stable IDs} when
+ * this method is used. This can help for the purposes of animation and visual
+ * object persistence but individual item views will still need to be rebound
+ * and relaid out.</p>
+ *
+ * <p>If you are writing an adapter it will always be more efficient to use the more
+ * specific change events if you can. Rely on <code>notifyDataSetChanged()</code>
+ * as a last resort.</p>
+ *
+ * @see #notifyItemChanged(int)
+ * @see #notifyItemInserted(int)
+ * @see #notifyItemRemoved(int)
+ * @see #notifyItemRangeChanged(int, int)
+ * @see #notifyItemRangeInserted(int, int)
+ * @see #notifyItemRangeRemoved(int, int)
+ */
+ public final void notifyDataSetChanged() {
+ mObservable.notifyChanged();
+ }
+
+ /**
+ * Notify any registered observers that the item at <code>position</code> has changed.
+ * Equivalent to calling <code>notifyItemChanged(position, null);</code>.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that any
+ * reflection of the data at <code>position</code> is out of date and should be updated.
+ * The item at <code>position</code> retains the same identity.</p>
+ *
+ * @param position Position of the item that has changed
+ *
+ * @see #notifyItemRangeChanged(int, int)
+ */
+ public final void notifyItemChanged(int position) {
+ mObservable.notifyItemRangeChanged(position, 1);
+ }
+
+ /**
+ * Notify any registered observers that the item at <code>position</code> has changed with
+ * an optional payload object.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that any
+ * reflection of the data at <code>position</code> is out of date and should be updated.
+ * The item at <code>position</code> retains the same identity.
+ * </p>
+ *
+ * <p>
+ * Client can optionally pass a payload for partial change. These payloads will be merged
+ * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
+ * item is already represented by a ViewHolder and it will be rebound to the same
+ * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
+ * payloads on that item and prevent future payload until
+ * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
+ * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
+ * attached, the payload will be simply dropped.
+ *
+ * @param position Position of the item that has changed
+ * @param payload Optional parameter, use null to identify a "full" update
+ *
+ * @see #notifyItemRangeChanged(int, int)
+ */
+ public final void notifyItemChanged(int position, Object payload) {
+ mObservable.notifyItemRangeChanged(position, 1, payload);
+ }
+
+ /**
+ * Notify any registered observers that the <code>itemCount</code> items starting at
+ * position <code>positionStart</code> have changed.
+ * Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that
+ * any reflection of the data in the given position range is out of date and should
+ * be updated. The items in the given range retain the same identity.</p>
+ *
+ * @param positionStart Position of the first item that has changed
+ * @param itemCount Number of items that have changed
+ *
+ * @see #notifyItemChanged(int)
+ */
+ public final void notifyItemRangeChanged(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ /**
+ * Notify any registered observers that the <code>itemCount</code> items starting at
+ * position <code>positionStart</code> have changed. An optional payload can be
+ * passed to each changed item.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that any
+ * reflection of the data in the given position range is out of date and should be updated.
+ * The items in the given range retain the same identity.
+ * </p>
+ *
+ * <p>
+ * Client can optionally pass a payload for partial change. These payloads will be merged
+ * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
+ * item is already represented by a ViewHolder and it will be rebound to the same
+ * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
+ * payloads on that item and prevent future payload until
+ * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
+ * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
+ * attached, the payload will be simply dropped.
+ *
+ * @param positionStart Position of the first item that has changed
+ * @param itemCount Number of items that have changed
+ * @param payload Optional parameter, use null to identify a "full" update
+ *
+ * @see #notifyItemChanged(int)
+ */
+ public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
+ }
+
+ /**
+ * Notify any registered observers that the item reflected at <code>position</code>
+ * has been newly inserted. The item previously at <code>position</code> is now at
+ * position <code>position + 1</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their
+ * positions may be altered.</p>
+ *
+ * @param position Position of the newly inserted item in the data set
+ *
+ * @see #notifyItemRangeInserted(int, int)
+ */
+ public final void notifyItemInserted(int position) {
+ mObservable.notifyItemRangeInserted(position, 1);
+ }
+
+ /**
+ * Notify any registered observers that the item reflected at <code>fromPosition</code>
+ * has been moved to <code>toPosition</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their
+ * positions may be altered.</p>
+ *
+ * @param fromPosition Previous position of the item.
+ * @param toPosition New position of the item.
+ */
+ public final void notifyItemMoved(int fromPosition, int toPosition) {
+ mObservable.notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /**
+ * Notify any registered observers that the currently reflected <code>itemCount</code>
+ * items starting at <code>positionStart</code> have been newly inserted. The items
+ * previously located at <code>positionStart</code> and beyond can now be found starting
+ * at position <code>positionStart + itemCount</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their positions
+ * may be altered.</p>
+ *
+ * @param positionStart Position of the first item that was inserted
+ * @param itemCount Number of items inserted
+ *
+ * @see #notifyItemInserted(int)
+ */
+ public final void notifyItemRangeInserted(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ /**
+ * Notify any registered observers that the item previously located at <code>position</code>
+ * has been removed from the data set. The items previously located at and after
+ * <code>position</code> may now be found at <code>oldPosition - 1</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their positions
+ * may be altered.</p>
+ *
+ * @param position Position of the item that has now been removed
+ *
+ * @see #notifyItemRangeRemoved(int, int)
+ */
+ public final void notifyItemRemoved(int position) {
+ mObservable.notifyItemRangeRemoved(position, 1);
+ }
+
+ /**
+ * Notify any registered observers that the <code>itemCount</code> items previously
+ * located at <code>positionStart</code> have been removed from the data set. The items
+ * previously located at and after <code>positionStart + itemCount</code> may now be found
+ * at <code>oldPosition - itemCount</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the data
+ * set are still considered up to date and will not be rebound, though their positions
+ * may be altered.</p>
+ *
+ * @param positionStart Previous position of the first item that was removed
+ * @param itemCount Number of items removed from the data set
+ */
+ public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ void dispatchChildDetached(View child) {
+ final ViewHolder viewHolder = getChildViewHolderInt(child);
+ onChildDetachedFromWindow(child);
+ if (mAdapter != null && viewHolder != null) {
+ mAdapter.onViewDetachedFromWindow(viewHolder);
+ }
+ if (mOnChildAttachStateListeners != null) {
+ final int cnt = mOnChildAttachStateListeners.size();
+ for (int i = cnt - 1; i >= 0; i--) {
+ mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child);
+ }
+ }
+ }
+
+ void dispatchChildAttached(View child) {
+ final ViewHolder viewHolder = getChildViewHolderInt(child);
+ onChildAttachedToWindow(child);
+ if (mAdapter != null && viewHolder != null) {
+ mAdapter.onViewAttachedToWindow(viewHolder);
+ }
+ if (mOnChildAttachStateListeners != null) {
+ final int cnt = mOnChildAttachStateListeners.size();
+ for (int i = cnt - 1; i >= 0; i--) {
+ mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child);
+ }
+ }
+ }
+
+ /**
+ * A <code>LayoutManager</code> is responsible for measuring and positioning item views
+ * within a <code>RecyclerView</code> as well as determining the policy for when to recycle
+ * item views that are no longer visible to the user. By changing the <code>LayoutManager</code>
+ * a <code>RecyclerView</code> can be used to implement a standard vertically scrolling list,
+ * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock
+ * layout managers are provided for general use.
+ * <p/>
+ * If the LayoutManager specifies a default constructor or one with the signature
+ * ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will
+ * instantiate and set the LayoutManager when being inflated. Most used properties can
+ * be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case
+ * a LayoutManager specifies both constructors, the non-default constructor will take
+ * precedence.
+ *
+ */
+ public abstract static class LayoutManager {
+ ChildHelper mChildHelper;
+ RecyclerView mRecyclerView;
+
+ @Nullable
+ SmoothScroller mSmoothScroller;
+
+ boolean mRequestedSimpleAnimations = false;
+
+ boolean mIsAttachedToWindow = false;
+
+ boolean mAutoMeasure = false;
+
+ /**
+ * LayoutManager has its own more strict measurement cache to avoid re-measuring a child
+ * if the space that will be given to it is already larger than what it has measured before.
+ */
+ private boolean mMeasurementCacheEnabled = true;
+
+ private boolean mItemPrefetchEnabled = true;
+
+ /**
+ * Written by {@link GapWorker} when prefetches occur to track largest number of view ever
+ * requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or
+ * {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call.
+ *
+ * If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)},
+ * will be reset upon layout to prevent initial prefetches (often large, since they're
+ * proportional to expected child count) from expanding cache permanently.
+ */
+ int mPrefetchMaxCountObserved;
+
+ /**
+ * If true, mPrefetchMaxCountObserved is only valid until next layout, and should be reset.
+ */
+ boolean mPrefetchMaxObservedInInitialPrefetch;
+
+ /**
+ * These measure specs might be the measure specs that were passed into RecyclerView's
+ * onMeasure method OR fake measure specs created by the RecyclerView.
+ * For example, when a layout is run, RecyclerView always sets these specs to be
+ * EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass.
+ * <p>
+ * Also, to be able to use the hint in unspecified measure specs, RecyclerView checks the
+ * API level and sets the size to 0 pre-M to avoid any issue that might be caused by
+ * corrupt values. Older platforms have no responsibility to provide a size if they set
+ * mode to unspecified.
+ */
+ private int mWidthMode, mHeightMode;
+ private int mWidth, mHeight;
+
+
+ /**
+ * Interface for LayoutManagers to request items to be prefetched, based on position, with
+ * specified distance from viewport, which indicates priority.
+ *
+ * @see LayoutManager#collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)
+ * @see LayoutManager#collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+ */
+ public interface LayoutPrefetchRegistry {
+ /**
+ * Requests an an item to be prefetched, based on position, with a specified distance,
+ * indicating priority.
+ *
+ * @param layoutPosition Position of the item to prefetch.
+ * @param pixelDistance Distance from the current viewport to the bounds of the item,
+ * must be non-negative.
+ */
+ void addPosition(int layoutPosition, int pixelDistance);
+ }
+
+ void setRecyclerView(RecyclerView recyclerView) {
+ if (recyclerView == null) {
+ mRecyclerView = null;
+ mChildHelper = null;
+ mWidth = 0;
+ mHeight = 0;
+ } else {
+ mRecyclerView = recyclerView;
+ mChildHelper = recyclerView.mChildHelper;
+ mWidth = recyclerView.getWidth();
+ mHeight = recyclerView.getHeight();
+ }
+ mWidthMode = MeasureSpec.EXACTLY;
+ mHeightMode = MeasureSpec.EXACTLY;
+ }
+
+ void setMeasureSpecs(int wSpec, int hSpec) {
+ mWidth = MeasureSpec.getSize(wSpec);
+ mWidthMode = MeasureSpec.getMode(wSpec);
+ if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
+ mWidth = 0;
+ }
+
+ mHeight = MeasureSpec.getSize(hSpec);
+ mHeightMode = MeasureSpec.getMode(hSpec);
+ if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
+ mHeight = 0;
+ }
+ }
+
+ /**
+ * Called after a layout is calculated during a measure pass when using auto-measure.
+ * <p>
+ * It simply traverses all children to calculate a bounding box then calls
+ * {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method
+ * if they need to handle the bounding box differently.
+ * <p>
+ * For example, GridLayoutManager override that method to ensure that even if a column is
+ * empty, the GridLayoutManager still measures wide enough to include it.
+ *
+ * @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure
+ * @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure
+ */
+ void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
+ final int count = getChildCount();
+ if (count == 0) {
+ mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
+ return;
+ }
+ int minX = Integer.MAX_VALUE;
+ int minY = Integer.MAX_VALUE;
+ int maxX = Integer.MIN_VALUE;
+ int maxY = Integer.MIN_VALUE;
+
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ final Rect bounds = mRecyclerView.mTempRect;
+ getDecoratedBoundsWithMargins(child, bounds);
+ if (bounds.left < minX) {
+ minX = bounds.left;
+ }
+ if (bounds.right > maxX) {
+ maxX = bounds.right;
+ }
+ if (bounds.top < minY) {
+ minY = bounds.top;
+ }
+ if (bounds.bottom > maxY) {
+ maxY = bounds.bottom;
+ }
+ }
+ mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
+ setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
+ }
+
+ /**
+ * Sets the measured dimensions from the given bounding box of the children and the
+ * measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is
+ * called after the RecyclerView calls
+ * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a measurement pass.
+ * <p>
+ * This method should call {@link #setMeasuredDimension(int, int)}.
+ * <p>
+ * The default implementation adds the RecyclerView's padding to the given bounding box
+ * then caps the value to be within the given measurement specs.
+ * <p>
+ * This method is only called if the LayoutManager opted into the auto measurement API.
+ *
+ * @param childrenBounds The bounding box of all children
+ * @param wSpec The widthMeasureSpec that was passed into the RecyclerView.
+ * @param hSpec The heightMeasureSpec that was passed into the RecyclerView.
+ *
+ * @see #setAutoMeasureEnabled(boolean)
+ */
+ public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
+ int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
+ int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
+ int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
+ int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView
+ */
+ public void requestLayout() {
+ if (mRecyclerView != null) {
+ mRecyclerView.requestLayout();
+ }
+ }
+
+ /**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is not</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertNotInLayoutOrScroll(String)
+ */
+ public void assertInLayoutOrScroll(String message) {
+ if (mRecyclerView != null) {
+ mRecyclerView.assertInLayoutOrScroll(message);
+ }
+ }
+
+ /**
+ * Chooses a size from the given specs and parameters that is closest to the desired size
+ * and also complies with the spec.
+ *
+ * @param spec The measureSpec
+ * @param desired The preferred measurement
+ * @param min The minimum value
+ *
+ * @return A size that fits to the given specs
+ */
+ public static int chooseSize(int spec, int desired, int min) {
+ final int mode = View.MeasureSpec.getMode(spec);
+ final int size = View.MeasureSpec.getSize(spec);
+ switch (mode) {
+ case View.MeasureSpec.EXACTLY:
+ return size;
+ case View.MeasureSpec.AT_MOST:
+ return Math.min(size, Math.max(desired, min));
+ case View.MeasureSpec.UNSPECIFIED:
+ default:
+ return Math.max(desired, min);
+ }
+ }
+
+ /**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertInLayoutOrScroll(String)
+ */
+ public void assertNotInLayoutOrScroll(String message) {
+ if (mRecyclerView != null) {
+ mRecyclerView.assertNotInLayoutOrScroll(message);
+ }
+ }
+
+ /**
+ * Defines whether the layout should be measured by the RecyclerView or the LayoutManager
+ * wants to handle the layout measurements itself.
+ * <p>
+ * This method is usually called by the LayoutManager with value {@code true} if it wants
+ * to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize
+ * the measurement logic, you can call this method with {@code false} and override
+ * {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic.
+ * <p>
+ * AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or
+ * handle various specs provided by the RecyclerView's parent.
+ * It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an
+ * {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based
+ * on children's positions. It does this while supporting all existing animation
+ * capabilities of the RecyclerView.
+ * <p>
+ * AutoMeasure works as follows:
+ * <ol>
+ * <li>LayoutManager should call {@code setAutoMeasureEnabled(true)} to enable it. All of
+ * the framework LayoutManagers use {@code auto-measure}.</li>
+ * <li>When {@link RecyclerView#onMeasure(int, int)} is called, if the provided specs are
+ * exact, RecyclerView will only call LayoutManager's {@code onMeasure} and return without
+ * doing any layout calculation.</li>
+ * <li>If one of the layout specs is not {@code EXACT}, the RecyclerView will start the
+ * layout process in {@code onMeasure} call. It will process all pending Adapter updates and
+ * decide whether to run a predictive layout or not. If it decides to do so, it will first
+ * call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to
+ * {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still
+ * return the width and height of the RecyclerView as of the last layout calculation.
+ * <p>
+ * After handling the predictive case, RecyclerView will call
+ * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
+ * {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can
+ * access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()},
+ * {@link #getWidth()} and {@link #getWidthMode()}.</li>
+ * <li>After the layout calculation, RecyclerView sets the measured width & height by
+ * calculating the bounding box for the children (+ RecyclerView's padding). The
+ * LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose
+ * different values. For instance, GridLayoutManager overrides this value to handle the case
+ * where if it is vertical and has 3 columns but only 2 items, it should still measure its
+ * width to fit 3 items, not 2.</li>
+ * <li>Any following on measure call to the RecyclerView will run
+ * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
+ * {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will
+ * take care of which views are actually added / removed / moved / changed for animations so
+ * that the LayoutManager should not worry about them and handle each
+ * {@link #onLayoutChildren(Recycler, State)} call as if it is the last one.
+ * </li>
+ * <li>When measure is complete and RecyclerView's
+ * {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks
+ * whether it already did layout calculations during the measure pass and if so, it re-uses
+ * that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)}
+ * if the last measure spec was different from the final dimensions or adapter contents
+ * have changed between the measure call and the layout call.</li>
+ * <li>Finally, animations are calculated and run as usual.</li>
+ * </ol>
+ *
+ * @param enabled <code>True</code> if the Layout should be measured by the
+ * RecyclerView, <code>false</code> if the LayoutManager wants
+ * to measure itself.
+ *
+ * @see #setMeasuredDimension(Rect, int, int)
+ * @see #isAutoMeasureEnabled()
+ */
+ public void setAutoMeasureEnabled(boolean enabled) {
+ mAutoMeasure = enabled;
+ }
+
+ /**
+ * Returns whether the LayoutManager uses the automatic measurement API or not.
+ *
+ * @return <code>True</code> if the LayoutManager is measured by the RecyclerView or
+ * <code>false</code> if it measures itself.
+ *
+ * @see #setAutoMeasureEnabled(boolean)
+ */
+ public boolean isAutoMeasureEnabled() {
+ return mAutoMeasure;
+ }
+
+ /**
+ * Returns whether this LayoutManager supports automatic item animations.
+ * A LayoutManager wishing to support item animations should obey certain
+ * rules as outlined in {@link #onLayoutChildren(Recycler, State)}.
+ * The default return value is <code>false</code>, so subclasses of LayoutManager
+ * will not get predictive item animations by default.
+ *
+ * <p>Whether item animations are enabled in a RecyclerView is determined both
+ * by the return value from this method and the
+ * {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the
+ * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this
+ * method returns false, then simple item animations will be enabled, in which
+ * views that are moving onto or off of the screen are simply faded in/out. If
+ * the RecyclerView has a non-null ItemAnimator and this method returns true,
+ * then there will be two calls to {@link #onLayoutChildren(Recycler, State)} to
+ * setup up the information needed to more intelligently predict where appearing
+ * and disappearing views should be animated from/to.</p>
+ *
+ * @return true if predictive item animations should be enabled, false otherwise
+ */
+ public boolean supportsPredictiveItemAnimations() {
+ return false;
+ }
+
+ /**
+ * Sets whether the LayoutManager should be queried for views outside of
+ * its viewport while the UI thread is idle between frames.
+ *
+ * <p>If enabled, the LayoutManager will be queried for items to inflate/bind in between
+ * view system traversals on devices running API 21 or greater. Default value is true.</p>
+ *
+ * <p>On platforms API level 21 and higher, the UI thread is idle between passing a frame
+ * to RenderThread and the starting up its next frame at the next VSync pulse. By
+ * prefetching out of window views in this time period, delays from inflation and view
+ * binding are much less likely to cause jank and stuttering during scrolls and flings.</p>
+ *
+ * <p>While prefetch is enabled, it will have the side effect of expanding the effective
+ * size of the View cache to hold prefetched views.</p>
+ *
+ * @param enabled <code>True</code> if items should be prefetched in between traversals.
+ *
+ * @see #isItemPrefetchEnabled()
+ */
+ public final void setItemPrefetchEnabled(boolean enabled) {
+ if (enabled != mItemPrefetchEnabled) {
+ mItemPrefetchEnabled = enabled;
+ mPrefetchMaxCountObserved = 0;
+ if (mRecyclerView != null) {
+ mRecyclerView.mRecycler.updateViewCacheSize();
+ }
+ }
+ }
+
+ /**
+ * Sets whether the LayoutManager should be queried for views outside of
+ * its viewport while the UI thread is idle between frames.
+ *
+ * @see #setItemPrefetchEnabled(boolean)
+ *
+ * @return true if item prefetch is enabled, false otherwise
+ */
+ public final boolean isItemPrefetchEnabled() {
+ return mItemPrefetchEnabled;
+ }
+
+ /**
+ * Gather all positions from the LayoutManager to be prefetched, given specified momentum.
+ *
+ * <p>If item prefetch is enabled, this method is called in between traversals to gather
+ * which positions the LayoutManager will soon need, given upcoming movement in subsequent
+ * traversals.</p>
+ *
+ * <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for
+ * each item to be prepared, and these positions will have their ViewHolders created and
+ * bound, if there is sufficient time available, in advance of being needed by a
+ * scroll or layout.</p>
+ *
+ * @param dx X movement component.
+ * @param dy Y movement component.
+ * @param state State of RecyclerView
+ * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into.
+ *
+ * @see #isItemPrefetchEnabled()
+ * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
+ */
+ public void collectAdjacentPrefetchPositions(int dx, int dy, State state,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {}
+
+ /**
+ * Gather all positions from the LayoutManager to be prefetched in preperation for its
+ * RecyclerView to come on screen, due to the movement of another, containing RecyclerView.
+ *
+ * <p>This method is only called when a RecyclerView is nested in another RecyclerView.</p>
+ *
+ * <p>If item prefetch is enabled for this LayoutManager, as well in another containing
+ * LayoutManager, this method is called in between draw traversals to gather
+ * which positions this LayoutManager will first need, once it appears on the screen.</p>
+ *
+ * <p>For example, if this LayoutManager represents a horizontally scrolling list within a
+ * vertically scrolling LayoutManager, this method would be called when the horizontal list
+ * is about to come onscreen.</p>
+ *
+ * <p>The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for
+ * each item to be prepared, and these positions will have their ViewHolders created and
+ * bound, if there is sufficient time available, in advance of being needed by a
+ * scroll or layout.</p>
+ *
+ * @param adapterItemCount number of items in the associated adapter.
+ * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into.
+ *
+ * @see #isItemPrefetchEnabled()
+ * @see #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)
+ */
+ public void collectInitialPrefetchPositions(int adapterItemCount,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {}
+
+ void dispatchAttachedToWindow(RecyclerView view) {
+ mIsAttachedToWindow = true;
+ onAttachedToWindow(view);
+ }
+
+ void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) {
+ mIsAttachedToWindow = false;
+ onDetachedFromWindow(view, recycler);
+ }
+
+ /**
+ * Returns whether LayoutManager is currently attached to a RecyclerView which is attached
+ * to a window.
+ *
+ * @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView
+ * is attached to window.
+ */
+ public boolean isAttachedToWindow() {
+ return mIsAttachedToWindow;
+ }
+
+ /**
+ * Causes the Runnable to execute on the next animation time step.
+ * The runnable will be run on the user interface thread.
+ * <p>
+ * Calling this method when LayoutManager is not attached to a RecyclerView has no effect.
+ *
+ * @param action The Runnable that will be executed.
+ *
+ * @see #removeCallbacks
+ */
+ public void postOnAnimation(Runnable action) {
+ if (mRecyclerView != null) {
+ mRecyclerView.postOnAnimation(action);
+ }
+ }
+
+ /**
+ * Removes the specified Runnable from the message queue.
+ * <p>
+ * Calling this method when LayoutManager is not attached to a RecyclerView has no effect.
+ *
+ * @param action The Runnable to remove from the message handling queue
+ *
+ * @return true if RecyclerView could ask the Handler to remove the Runnable,
+ * false otherwise. When the returned value is true, the Runnable
+ * may or may not have been actually removed from the message queue
+ * (for instance, if the Runnable was not in the queue already.)
+ *
+ * @see #postOnAnimation
+ */
+ public boolean removeCallbacks(Runnable action) {
+ if (mRecyclerView != null) {
+ return mRecyclerView.removeCallbacks(action);
+ }
+ return false;
+ }
+ /**
+ * Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView
+ * is attached to a window.
+ * <p>
+ * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not
+ * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was
+ * not requested on the RecyclerView while it was detached.
+ * <p>
+ * Subclass implementations should always call through to the superclass implementation.
+ *
+ * @param view The RecyclerView this LayoutManager is bound to
+ *
+ * @see #onDetachedFromWindow(RecyclerView, Recycler)
+ */
+ @CallSuper
+ public void onAttachedToWindow(RecyclerView view) {
+ }
+
+ /**
+ * @deprecated
+ * override {@link #onDetachedFromWindow(RecyclerView, Recycler)}
+ */
+ @Deprecated
+ public void onDetachedFromWindow(RecyclerView view) {
+
+ }
+
+ /**
+ * Called when this LayoutManager is detached from its parent RecyclerView or when
+ * its parent RecyclerView is detached from its window.
+ * <p>
+ * LayoutManager should clear all of its View references as another LayoutManager might be
+ * assigned to the RecyclerView.
+ * <p>
+ * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not
+ * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was
+ * not requested on the RecyclerView while it was detached.
+ * <p>
+ * If your LayoutManager has View references that it cleans in on-detach, it should also
+ * call {@link RecyclerView#requestLayout()} to ensure that it is re-laid out when
+ * RecyclerView is re-attached.
+ * <p>
+ * Subclass implementations should always call through to the superclass implementation.
+ *
+ * @param view The RecyclerView this LayoutManager is bound to
+ * @param recycler The recycler to use if you prefer to recycle your children instead of
+ * keeping them around.
+ *
+ * @see #onAttachedToWindow(RecyclerView)
+ */
+ @CallSuper
+ public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
+ onDetachedFromWindow(view);
+ }
+
+ /**
+ * Check if the RecyclerView is configured to clip child views to its padding.
+ *
+ * @return true if this RecyclerView clips children to its padding, false otherwise
+ */
+ public boolean getClipToPadding() {
+ return mRecyclerView != null && mRecyclerView.mClipToPadding;
+ }
+
+ /**
+ * Lay out all relevant child views from the given adapter.
+ *
+ * The LayoutManager is in charge of the behavior of item animations. By default,
+ * RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple
+ * item animations are enabled. This means that add/remove operations on the
+ * adapter will result in animations to add new or appearing items, removed or
+ * disappearing items, and moved items. If a LayoutManager returns false from
+ * {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a
+ * normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the
+ * RecyclerView will have enough information to run those animations in a simple
+ * way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will
+ * simply fade views in and out, whether they are actually added/removed or whether
+ * they are moved on or off the screen due to other add/remove operations.
+ *
+ * <p>A LayoutManager wanting a better item animation experience, where items can be
+ * animated onto and off of the screen according to where the items exist when they
+ * are not on screen, then the LayoutManager should return true from
+ * {@link #supportsPredictiveItemAnimations()} and add additional logic to
+ * {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations
+ * means that {@link #onLayoutChildren(Recycler, State)} will be called twice;
+ * once as a "pre" layout step to determine where items would have been prior to
+ * a real layout, and again to do the "real" layout. In the pre-layout phase,
+ * items will remember their pre-layout positions to allow them to be laid out
+ * appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will
+ * be returned from the scrap to help determine correct placement of other items.
+ * These removed items should not be added to the child list, but should be used
+ * to help calculate correct positioning of other views, including views that
+ * were not previously onscreen (referred to as APPEARING views), but whose
+ * pre-layout offscreen position can be determined given the extra
+ * information about the pre-layout removed views.</p>
+ *
+ * <p>The second layout pass is the real layout in which only non-removed views
+ * will be used. The only additional requirement during this pass is, if
+ * {@link #supportsPredictiveItemAnimations()} returns true, to note which
+ * views exist in the child list prior to layout and which are not there after
+ * layout (referred to as DISAPPEARING views), and to position/layout those views
+ * appropriately, without regard to the actual bounds of the RecyclerView. This allows
+ * the animation system to know the location to which to animate these disappearing
+ * views.</p>
+ *
+ * <p>The default LayoutManager implementations for RecyclerView handle all of these
+ * requirements for animations already. Clients of RecyclerView can either use one
+ * of these layout managers directly or look at their implementations of
+ * onLayoutChildren() to see how they account for the APPEARING and
+ * DISAPPEARING views.</p>
+ *
+ * @param recycler Recycler to use for fetching potentially cached views for a
+ * position
+ * @param state Transient state of RecyclerView
+ */
+ public void onLayoutChildren(Recycler recycler, State state) {
+ Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
+ }
+
+ /**
+ * Called after a full layout calculation is finished. The layout calculation may include
+ * multiple {@link #onLayoutChildren(Recycler, State)} calls due to animations or
+ * layout measurement but it will include only one {@link #onLayoutCompleted(State)} call.
+ * This method will be called at the end of {@link View#layout(int, int, int, int)} call.
+ * <p>
+ * This is a good place for the LayoutManager to do some cleanup like pending scroll
+ * position, saved state etc.
+ *
+ * @param state Transient state of RecyclerView
+ */
+ public void onLayoutCompleted(State state) {
+ }
+
+ /**
+ * Create a default <code>LayoutParams</code> object for a child of the RecyclerView.
+ *
+ * <p>LayoutManagers will often want to use a custom <code>LayoutParams</code> type
+ * to store extra information specific to the layout. Client code should subclass
+ * {@link RecyclerView.LayoutParams} for this purpose.</p>
+ *
+ * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+ * you must also override
+ * {@link #checkLayoutParams(LayoutParams)},
+ * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+ * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+ *
+ * @return A new LayoutParams for a child view
+ */
+ public abstract LayoutParams generateDefaultLayoutParams();
+
+ /**
+ * Determines the validity of the supplied LayoutParams object.
+ *
+ * <p>This should check to make sure that the object is of the correct type
+ * and all values are within acceptable ranges. The default implementation
+ * returns <code>true</code> for non-null params.</p>
+ *
+ * @param lp LayoutParams object to check
+ * @return true if this LayoutParams object is valid, false otherwise
+ */
+ public boolean checkLayoutParams(LayoutParams lp) {
+ return lp != null;
+ }
+
+ /**
+ * Create a LayoutParams object suitable for this LayoutManager, copying relevant
+ * values from the supplied LayoutParams object if possible.
+ *
+ * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+ * you must also override
+ * {@link #checkLayoutParams(LayoutParams)},
+ * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+ * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+ *
+ * @param lp Source LayoutParams object to copy values from
+ * @return a new LayoutParams object
+ */
+ public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ } else {
+ return new LayoutParams(lp);
+ }
+ }
+
+ /**
+ * Create a LayoutParams object suitable for this LayoutManager from
+ * an inflated layout resource.
+ *
+ * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+ * you must also override
+ * {@link #checkLayoutParams(LayoutParams)},
+ * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+ * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+ *
+ * @param c Context for obtaining styled attributes
+ * @param attrs AttributeSet describing the supplied arguments
+ * @return a new LayoutParams object
+ */
+ public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
+ return new LayoutParams(c, attrs);
+ }
+
+ /**
+ * Scroll horizontally by dx pixels in screen coordinates and return the distance traveled.
+ * The default implementation does nothing and returns 0.
+ *
+ * @param dx distance to scroll by in pixels. X increases as scroll position
+ * approaches the right.
+ * @param recycler Recycler to use for fetching potentially cached views for a
+ * position
+ * @param state Transient state of RecyclerView
+ * @return The actual distance scrolled. The return value will be negative if dx was
+ * negative and scrolling proceeeded in that direction.
+ * <code>Math.abs(result)</code> may be less than dx if a boundary was reached.
+ */
+ public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
+ return 0;
+ }
+
+ /**
+ * Scroll vertically by dy pixels in screen coordinates and return the distance traveled.
+ * The default implementation does nothing and returns 0.
+ *
+ * @param dy distance to scroll in pixels. Y increases as scroll position
+ * approaches the bottom.
+ * @param recycler Recycler to use for fetching potentially cached views for a
+ * position
+ * @param state Transient state of RecyclerView
+ * @return The actual distance scrolled. The return value will be negative if dy was
+ * negative and scrolling proceeeded in that direction.
+ * <code>Math.abs(result)</code> may be less than dy if a boundary was reached.
+ */
+ public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
+ return 0;
+ }
+
+ /**
+ * Query if horizontal scrolling is currently supported. The default implementation
+ * returns false.
+ *
+ * @return True if this LayoutManager can scroll the current contents horizontally
+ */
+ public boolean canScrollHorizontally() {
+ return false;
+ }
+
+ /**
+ * Query if vertical scrolling is currently supported. The default implementation
+ * returns false.
+ *
+ * @return True if this LayoutManager can scroll the current contents vertically
+ */
+ public boolean canScrollVertically() {
+ return false;
+ }
+
+ /**
+ * Scroll to the specified adapter position.
+ *
+ * Actual position of the item on the screen depends on the LayoutManager implementation.
+ * @param position Scroll to this adapter position.
+ */
+ public void scrollToPosition(int position) {
+ if (DEBUG) {
+ Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract");
+ }
+ }
+
+ /**
+ * <p>Smooth scroll to the specified adapter position.</p>
+ * <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
+ * instance and call {@link #startSmoothScroll(SmoothScroller)}.
+ * </p>
+ * @param recyclerView The RecyclerView to which this layout manager is attached
+ * @param state Current State of RecyclerView
+ * @param position Scroll to this adapter position.
+ */
+ public void smoothScrollToPosition(RecyclerView recyclerView, State state,
+ int position) {
+ Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
+ }
+
+ /**
+ * <p>Starts a smooth scroll using the provided SmoothScroller.</p>
+ * <p>Calling this method will cancel any previous smooth scroll request.</p>
+ * @param smoothScroller Instance which defines how smooth scroll should be animated
+ */
+ public void startSmoothScroll(SmoothScroller smoothScroller) {
+ if (mSmoothScroller != null && smoothScroller != mSmoothScroller
+ && mSmoothScroller.isRunning()) {
+ mSmoothScroller.stop();
+ }
+ mSmoothScroller = smoothScroller;
+ mSmoothScroller.start(mRecyclerView, this);
+ }
+
+ /**
+ * @return true if RecycylerView is currently in the state of smooth scrolling.
+ */
+ public boolean isSmoothScrolling() {
+ return mSmoothScroller != null && mSmoothScroller.isRunning();
+ }
+
+
+ /**
+ * Returns the resolved layout direction for this RecyclerView.
+ *
+ * @return {@link android.view.View#LAYOUT_DIRECTION_RTL} if the layout
+ * direction is RTL or returns
+ * {@link android.view.View#LAYOUT_DIRECTION_LTR} if the layout direction
+ * is not RTL.
+ */
+ public int getLayoutDirection() {
+ return mRecyclerView.getLayoutDirection();
+ }
+
+ /**
+ * Ends all animations on the view created by the {@link ItemAnimator}.
+ *
+ * @param view The View for which the animations should be ended.
+ * @see RecyclerView.ItemAnimator#endAnimations()
+ */
+ public void endAnimation(View view) {
+ if (mRecyclerView.mItemAnimator != null) {
+ mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view));
+ }
+ }
+
+ /**
+ * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
+ * to the layout that is known to be going away, either because it has been
+ * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
+ * visible portion of the container but is being laid out in order to inform RecyclerView
+ * in how to animate the item out of view.
+ * <p>
+ * Views added via this method are going to be invisible to LayoutManager after the
+ * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
+ * or won't be included in {@link #getChildCount()} method.
+ *
+ * @param child View to add and then remove with animation.
+ */
+ public void addDisappearingView(View child) {
+ addDisappearingView(child, -1);
+ }
+
+ /**
+ * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
+ * to the layout that is known to be going away, either because it has been
+ * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
+ * visible portion of the container but is being laid out in order to inform RecyclerView
+ * in how to animate the item out of view.
+ * <p>
+ * Views added via this method are going to be invisible to LayoutManager after the
+ * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
+ * or won't be included in {@link #getChildCount()} method.
+ *
+ * @param child View to add and then remove with animation.
+ * @param index Index of the view.
+ */
+ public void addDisappearingView(View child, int index) {
+ addViewInt(child, index, true);
+ }
+
+ /**
+ * Add a view to the currently attached RecyclerView if needed. LayoutManagers should
+ * use this method to add views obtained from a {@link Recycler} using
+ * {@link Recycler#getViewForPosition(int)}.
+ *
+ * @param child View to add
+ */
+ public void addView(View child) {
+ addView(child, -1);
+ }
+
+ /**
+ * Add a view to the currently attached RecyclerView if needed. LayoutManagers should
+ * use this method to add views obtained from a {@link Recycler} using
+ * {@link Recycler#getViewForPosition(int)}.
+ *
+ * @param child View to add
+ * @param index Index to add child at
+ */
+ public void addView(View child, int index) {
+ addViewInt(child, index, false);
+ }
+
+ private void addViewInt(View child, int index, boolean disappearing) {
+ final ViewHolder holder = getChildViewHolderInt(child);
+ if (disappearing || holder.isRemoved()) {
+ // these views will be hidden at the end of the layout pass.
+ mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
+ } else {
+ // This may look like unnecessary but may happen if layout manager supports
+ // predictive layouts and adapter removed then re-added the same item.
+ // In this case, added version will be visible in the post layout (because add is
+ // deferred) but RV will still bind it to the same View.
+ // So if a View re-appears in post layout pass, remove it from disappearing list.
+ mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
+ }
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (holder.wasReturnedFromScrap() || holder.isScrap()) {
+ if (holder.isScrap()) {
+ holder.unScrap();
+ } else {
+ holder.clearReturnedFromScrapFlag();
+ }
+ mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
+ if (DISPATCH_TEMP_DETACH) {
+ child.dispatchFinishTemporaryDetach();
+ }
+ } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
+ // ensure in correct position
+ int currentIndex = mChildHelper.indexOfChild(child);
+ if (index == -1) {
+ index = mChildHelper.getChildCount();
+ }
+ if (currentIndex == -1) {
+ throw new IllegalStateException("Added View has RecyclerView as parent but"
+ + " view is not a real child. Unfiltered index:"
+ + mRecyclerView.indexOfChild(child));
+ }
+ if (currentIndex != index) {
+ mRecyclerView.mLayout.moveView(currentIndex, index);
+ }
+ } else {
+ mChildHelper.addView(child, index, false);
+ lp.mInsetsDirty = true;
+ if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
+ mSmoothScroller.onChildAttachedToWindow(child);
+ }
+ }
+ if (lp.mPendingInvalidate) {
+ if (DEBUG) {
+ Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);
+ }
+ holder.itemView.invalidate();
+ lp.mPendingInvalidate = false;
+ }
+ }
+
+ /**
+ * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
+ * use this method to completely remove a child view that is no longer needed.
+ * LayoutManagers should strongly consider recycling removed views using
+ * {@link Recycler#recycleView(android.view.View)}.
+ *
+ * @param child View to remove
+ */
+ public void removeView(View child) {
+ mChildHelper.removeView(child);
+ }
+
+ /**
+ * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
+ * use this method to completely remove a child view that is no longer needed.
+ * LayoutManagers should strongly consider recycling removed views using
+ * {@link Recycler#recycleView(android.view.View)}.
+ *
+ * @param index Index of the child view to remove
+ */
+ public void removeViewAt(int index) {
+ final View child = getChildAt(index);
+ if (child != null) {
+ mChildHelper.removeViewAt(index);
+ }
+ }
+
+ /**
+ * Remove all views from the currently attached RecyclerView. This will not recycle
+ * any of the affected views; the LayoutManager is responsible for doing so if desired.
+ */
+ public void removeAllViews() {
+ // Only remove non-animating views
+ final int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ mChildHelper.removeViewAt(i);
+ }
+ }
+
+ /**
+ * Returns offset of the RecyclerView's text baseline from the its top boundary.
+ *
+ * @return The offset of the RecyclerView's text baseline from the its top boundary; -1 if
+ * there is no baseline.
+ */
+ public int getBaseline() {
+ return -1;
+ }
+
+ /**
+ * Returns the adapter position of the item represented by the given View. This does not
+ * contain any adapter changes that might have happened after the last layout.
+ *
+ * @param view The view to query
+ * @return The adapter position of the item which is rendered by this View.
+ */
+ public int getPosition(View view) {
+ return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
+ }
+
+ /**
+ * Returns the View type defined by the adapter.
+ *
+ * @param view The view to query
+ * @return The type of the view assigned by the adapter.
+ */
+ public int getItemViewType(View view) {
+ return getChildViewHolderInt(view).getItemViewType();
+ }
+
+ /**
+ * Traverses the ancestors of the given view and returns the item view that contains it
+ * and also a direct child of the LayoutManager.
+ * <p>
+ * Note that this method may return null if the view is a child of the RecyclerView but
+ * not a child of the LayoutManager (e.g. running a disappear animation).
+ *
+ * @param view The view that is a descendant of the LayoutManager.
+ *
+ * @return The direct child of the LayoutManager which contains the given view or null if
+ * the provided view is not a descendant of this LayoutManager.
+ *
+ * @see RecyclerView#getChildViewHolder(View)
+ * @see RecyclerView#findContainingViewHolder(View)
+ */
+ @Nullable
+ public View findContainingItemView(View view) {
+ if (mRecyclerView == null) {
+ return null;
+ }
+ View found = mRecyclerView.findContainingItemView(view);
+ if (found == null) {
+ return null;
+ }
+ if (mChildHelper.isHidden(found)) {
+ return null;
+ }
+ return found;
+ }
+
+ /**
+ * Finds the view which represents the given adapter position.
+ * <p>
+ * This method traverses each child since it has no information about child order.
+ * Override this method to improve performance if your LayoutManager keeps data about
+ * child views.
+ * <p>
+ * If a view is ignored via {@link #ignoreView(View)}, it is also ignored by this method.
+ *
+ * @param position Position of the item in adapter
+ * @return The child view that represents the given position or null if the position is not
+ * laid out
+ */
+ public View findViewByPosition(int position) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ ViewHolder vh = getChildViewHolderInt(child);
+ if (vh == null) {
+ continue;
+ }
+ if (vh.getLayoutPosition() == position && !vh.shouldIgnore()
+ && (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Temporarily detach a child view.
+ *
+ * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
+ * views currently attached to the RecyclerView. Generally LayoutManager implementations
+ * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
+ * so that the detached view may be rebound and reused.</p>
+ *
+ * <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
+ * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
+ * or {@link #removeDetachedView(android.view.View) fully remove} the detached view
+ * before the LayoutManager entry point method called by RecyclerView returns.</p>
+ *
+ * @param child Child to detach
+ */
+ public void detachView(View child) {
+ final int ind = mChildHelper.indexOfChild(child);
+ if (ind >= 0) {
+ detachViewInternal(ind, child);
+ }
+ }
+
+ /**
+ * Temporarily detach a child view.
+ *
+ * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
+ * views currently attached to the RecyclerView. Generally LayoutManager implementations
+ * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
+ * so that the detached view may be rebound and reused.</p>
+ *
+ * <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
+ * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
+ * or {@link #removeDetachedView(android.view.View) fully remove} the detached view
+ * before the LayoutManager entry point method called by RecyclerView returns.</p>
+ *
+ * @param index Index of the child to detach
+ */
+ public void detachViewAt(int index) {
+ detachViewInternal(index, getChildAt(index));
+ }
+
+ private void detachViewInternal(int index, View view) {
+ if (DISPATCH_TEMP_DETACH) {
+ view.dispatchStartTemporaryDetach();
+ }
+ mChildHelper.detachViewFromParent(index);
+ }
+
+ /**
+ * Reattach a previously {@link #detachView(android.view.View) detached} view.
+ * This method should not be used to reattach views that were previously
+ * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
+ *
+ * @param child Child to reattach
+ * @param index Intended child index for child
+ * @param lp LayoutParams for child
+ */
+ public void attachView(View child, int index, LayoutParams lp) {
+ ViewHolder vh = getChildViewHolderInt(child);
+ if (vh.isRemoved()) {
+ mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh);
+ } else {
+ mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh);
+ }
+ mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved());
+ if (DISPATCH_TEMP_DETACH) {
+ child.dispatchFinishTemporaryDetach();
+ }
+ }
+
+ /**
+ * Reattach a previously {@link #detachView(android.view.View) detached} view.
+ * This method should not be used to reattach views that were previously
+ * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
+ *
+ * @param child Child to reattach
+ * @param index Intended child index for child
+ */
+ public void attachView(View child, int index) {
+ attachView(child, index, (LayoutParams) child.getLayoutParams());
+ }
+
+ /**
+ * Reattach a previously {@link #detachView(android.view.View) detached} view.
+ * This method should not be used to reattach views that were previously
+ * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
+ *
+ * @param child Child to reattach
+ */
+ public void attachView(View child) {
+ attachView(child, -1);
+ }
+
+ /**
+ * Finish removing a view that was previously temporarily
+ * {@link #detachView(android.view.View) detached}.
+ *
+ * @param child Detached child to remove
+ */
+ public void removeDetachedView(View child) {
+ mRecyclerView.removeDetachedView(child, false);
+ }
+
+ /**
+ * Moves a View from one position to another.
+ *
+ * @param fromIndex The View's initial index
+ * @param toIndex The View's target index
+ */
+ public void moveView(int fromIndex, int toIndex) {
+ View view = getChildAt(fromIndex);
+ if (view == null) {
+ throw new IllegalArgumentException("Cannot move a child from non-existing index:"
+ + fromIndex);
+ }
+ detachViewAt(fromIndex);
+ attachView(view, toIndex);
+ }
+
+ /**
+ * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
+ *
+ * <p>Scrapping a view allows it to be rebound and reused to show updated or
+ * different data.</p>
+ *
+ * @param child Child to detach and scrap
+ * @param recycler Recycler to deposit the new scrap view into
+ */
+ public void detachAndScrapView(View child, Recycler recycler) {
+ int index = mChildHelper.indexOfChild(child);
+ scrapOrRecycleView(recycler, index, child);
+ }
+
+ /**
+ * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
+ *
+ * <p>Scrapping a view allows it to be rebound and reused to show updated or
+ * different data.</p>
+ *
+ * @param index Index of child to detach and scrap
+ * @param recycler Recycler to deposit the new scrap view into
+ */
+ public void detachAndScrapViewAt(int index, Recycler recycler) {
+ final View child = getChildAt(index);
+ scrapOrRecycleView(recycler, index, child);
+ }
+
+ /**
+ * Remove a child view and recycle it using the given Recycler.
+ *
+ * @param child Child to remove and recycle
+ * @param recycler Recycler to use to recycle child
+ */
+ public void removeAndRecycleView(View child, Recycler recycler) {
+ removeView(child);
+ recycler.recycleView(child);
+ }
+
+ /**
+ * Remove a child view and recycle it using the given Recycler.
+ *
+ * @param index Index of child to remove and recycle
+ * @param recycler Recycler to use to recycle child
+ */
+ public void removeAndRecycleViewAt(int index, Recycler recycler) {
+ final View view = getChildAt(index);
+ removeViewAt(index);
+ recycler.recycleView(view);
+ }
+
+ /**
+ * Return the current number of child views attached to the parent RecyclerView.
+ * This does not include child views that were temporarily detached and/or scrapped.
+ *
+ * @return Number of attached children
+ */
+ public int getChildCount() {
+ return mChildHelper != null ? mChildHelper.getChildCount() : 0;
+ }
+
+ /**
+ * Return the child view at the given index
+ * @param index Index of child to return
+ * @return Child view at index
+ */
+ public View getChildAt(int index) {
+ return mChildHelper != null ? mChildHelper.getChildAt(index) : null;
+ }
+
+ /**
+ * Return the width measurement spec mode of the RecyclerView.
+ * <p>
+ * This value is set only if the LayoutManager opts into the auto measure api via
+ * {@link #setAutoMeasureEnabled(boolean)}.
+ * <p>
+ * When RecyclerView is running a layout, this value is always set to
+ * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
+ *
+ * @return Width measure spec mode.
+ *
+ * @see View.MeasureSpec#getMode(int)
+ * @see View#onMeasure(int, int)
+ */
+ public int getWidthMode() {
+ return mWidthMode;
+ }
+
+ /**
+ * Return the height measurement spec mode of the RecyclerView.
+ * <p>
+ * This value is set only if the LayoutManager opts into the auto measure api via
+ * {@link #setAutoMeasureEnabled(boolean)}.
+ * <p>
+ * When RecyclerView is running a layout, this value is always set to
+ * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
+ *
+ * @return Height measure spec mode.
+ *
+ * @see View.MeasureSpec#getMode(int)
+ * @see View#onMeasure(int, int)
+ */
+ public int getHeightMode() {
+ return mHeightMode;
+ }
+
+ /**
+ * Return the width of the parent RecyclerView
+ *
+ * @return Width in pixels
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Return the height of the parent RecyclerView
+ *
+ * @return Height in pixels
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Return the left padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingLeft() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0;
+ }
+
+ /**
+ * Return the top padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingTop() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0;
+ }
+
+ /**
+ * Return the right padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingRight() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0;
+ }
+
+ /**
+ * Return the bottom padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingBottom() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0;
+ }
+
+ /**
+ * Return the start padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingStart() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingStart() : 0;
+ }
+
+ /**
+ * Return the end padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingEnd() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingEnd() : 0;
+ }
+
+ /**
+ * Returns true if the RecyclerView this LayoutManager is bound to has focus.
+ *
+ * @return True if the RecyclerView has focus, false otherwise.
+ * @see View#isFocused()
+ */
+ public boolean isFocused() {
+ return mRecyclerView != null && mRecyclerView.isFocused();
+ }
+
+ /**
+ * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus.
+ *
+ * @return true if the RecyclerView has or contains focus
+ * @see View#hasFocus()
+ */
+ public boolean hasFocus() {
+ return mRecyclerView != null && mRecyclerView.hasFocus();
+ }
+
+ /**
+ * Returns the item View which has or contains focus.
+ *
+ * @return A direct child of RecyclerView which has focus or contains the focused child.
+ */
+ public View getFocusedChild() {
+ if (mRecyclerView == null) {
+ return null;
+ }
+ final View focused = mRecyclerView.getFocusedChild();
+ if (focused == null || mChildHelper.isHidden(focused)) {
+ return null;
+ }
+ return focused;
+ }
+
+ /**
+ * Returns the number of items in the adapter bound to the parent RecyclerView.
+ * <p>
+ * Note that this number is not necessarily equal to
+ * {@link State#getItemCount() State#getItemCount()}. In methods where {@link State} is
+ * available, you should use {@link State#getItemCount() State#getItemCount()} instead.
+ * For more details, check the documentation for
+ * {@link State#getItemCount() State#getItemCount()}.
+ *
+ * @return The number of items in the bound adapter
+ * @see State#getItemCount()
+ */
+ public int getItemCount() {
+ final Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null;
+ return a != null ? a.getItemCount() : 0;
+ }
+
+ /**
+ * Offset all child views attached to the parent RecyclerView by dx pixels along
+ * the horizontal axis.
+ *
+ * @param dx Pixels to offset by
+ */
+ public void offsetChildrenHorizontal(int dx) {
+ if (mRecyclerView != null) {
+ mRecyclerView.offsetChildrenHorizontal(dx);
+ }
+ }
+
+ /**
+ * Offset all child views attached to the parent RecyclerView by dy pixels along
+ * the vertical axis.
+ *
+ * @param dy Pixels to offset by
+ */
+ public void offsetChildrenVertical(int dy) {
+ if (mRecyclerView != null) {
+ mRecyclerView.offsetChildrenVertical(dy);
+ }
+ }
+
+ /**
+ * Flags a view so that it will not be scrapped or recycled.
+ * <p>
+ * Scope of ignoring a child is strictly restricted to position tracking, scrapping and
+ * recyling. Methods like {@link #removeAndRecycleAllViews(Recycler)} will ignore the child
+ * whereas {@link #removeAllViews()} or {@link #offsetChildrenHorizontal(int)} will not
+ * ignore the child.
+ * <p>
+ * Before this child can be recycled again, you have to call
+ * {@link #stopIgnoringView(View)}.
+ * <p>
+ * You can call this method only if your LayoutManger is in onLayout or onScroll callback.
+ *
+ * @param view View to ignore.
+ * @see #stopIgnoringView(View)
+ */
+ public void ignoreView(View view) {
+ if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) {
+ // checking this because calling this method on a recycled or detached view may
+ // cause loss of state.
+ throw new IllegalArgumentException("View should be fully attached to be ignored");
+ }
+ final ViewHolder vh = getChildViewHolderInt(view);
+ vh.addFlags(ViewHolder.FLAG_IGNORE);
+ mRecyclerView.mViewInfoStore.removeViewHolder(vh);
+ }
+
+ /**
+ * View can be scrapped and recycled again.
+ * <p>
+ * Note that calling this method removes all information in the view holder.
+ * <p>
+ * You can call this method only if your LayoutManger is in onLayout or onScroll callback.
+ *
+ * @param view View to ignore.
+ */
+ public void stopIgnoringView(View view) {
+ final ViewHolder vh = getChildViewHolderInt(view);
+ vh.stopIgnoring();
+ vh.resetInternal();
+ vh.addFlags(ViewHolder.FLAG_INVALID);
+ }
+
+ /**
+ * Temporarily detach and scrap all currently attached child views. Views will be scrapped
+ * into the given Recycler. The Recycler may prefer to reuse scrap views before
+ * other views that were previously recycled.
+ *
+ * @param recycler Recycler to scrap views into
+ */
+ public void detachAndScrapAttachedViews(Recycler recycler) {
+ final int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View v = getChildAt(i);
+ scrapOrRecycleView(recycler, i, v);
+ }
+ }
+
+ private void scrapOrRecycleView(Recycler recycler, int index, View view) {
+ final ViewHolder viewHolder = getChildViewHolderInt(view);
+ if (viewHolder.shouldIgnore()) {
+ if (DEBUG) {
+ Log.d(TAG, "ignoring view " + viewHolder);
+ }
+ return;
+ }
+ if (viewHolder.isInvalid() && !viewHolder.isRemoved()
+ && !mRecyclerView.mAdapter.hasStableIds()) {
+ removeViewAt(index);
+ recycler.recycleViewHolderInternal(viewHolder);
+ } else {
+ detachViewAt(index);
+ recycler.scrapView(view);
+ mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
+ }
+ }
+
+ /**
+ * Recycles the scrapped views.
+ * <p>
+ * When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is
+ * the expected behavior if scrapped views are used for animations. Otherwise, we need to
+ * call remove and invalidate RecyclerView to ensure UI update.
+ *
+ * @param recycler Recycler
+ */
+ void removeAndRecycleScrapInt(Recycler recycler) {
+ final int scrapCount = recycler.getScrapCount();
+ // Loop backward, recycler might be changed by removeDetachedView()
+ for (int i = scrapCount - 1; i >= 0; i--) {
+ final View scrap = recycler.getScrapViewAt(i);
+ final ViewHolder vh = getChildViewHolderInt(scrap);
+ if (vh.shouldIgnore()) {
+ continue;
+ }
+ // If the scrap view is animating, we need to cancel them first. If we cancel it
+ // here, ItemAnimator callback may recycle it which will cause double recycling.
+ // To avoid this, we mark it as not recycleable before calling the item animator.
+ // Since removeDetachedView calls a user API, a common mistake (ending animations on
+ // the view) may recycle it too, so we guard it before we call user APIs.
+ vh.setIsRecyclable(false);
+ if (vh.isTmpDetached()) {
+ mRecyclerView.removeDetachedView(scrap, false);
+ }
+ if (mRecyclerView.mItemAnimator != null) {
+ mRecyclerView.mItemAnimator.endAnimation(vh);
+ }
+ vh.setIsRecyclable(true);
+ recycler.quickRecycleScrapView(scrap);
+ }
+ recycler.clearScrap();
+ if (scrapCount > 0) {
+ mRecyclerView.invalidate();
+ }
+ }
+
+
+ /**
+ * Measure a child view using standard measurement policy, taking the padding
+ * of the parent RecyclerView and any added item decorations into account.
+ *
+ * <p>If the RecyclerView can be scrolled in either dimension the caller may
+ * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
+ *
+ * @param child Child view to measure
+ * @param widthUsed Width in pixels currently consumed by other views, if relevant
+ * @param heightUsed Height in pixels currently consumed by other views, if relevant
+ */
+ public void measureChild(View child, int widthUsed, int heightUsed) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+ widthUsed += insets.left + insets.right;
+ heightUsed += insets.top + insets.bottom;
+ final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
+ getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
+ canScrollHorizontally());
+ final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
+ getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
+ canScrollVertically());
+ if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
+ child.measure(widthSpec, heightSpec);
+ }
+ }
+
+ /**
+ * RecyclerView internally does its own View measurement caching which should help with
+ * WRAP_CONTENT.
+ * <p>
+ * Use this method if the View is already measured once in this layout pass.
+ */
+ boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
+ return !mMeasurementCacheEnabled
+ || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width)
+ || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height);
+ }
+
+ // we may consider making this public
+ /**
+ * RecyclerView internally does its own View measurement caching which should help with
+ * WRAP_CONTENT.
+ * <p>
+ * Use this method if the View is not yet measured and you need to decide whether to
+ * measure this View or not.
+ */
+ boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
+ return child.isLayoutRequested()
+ || !mMeasurementCacheEnabled
+ || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width)
+ || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height);
+ }
+
+ /**
+ * In addition to the View Framework's measurement cache, RecyclerView uses its own
+ * additional measurement cache for its children to avoid re-measuring them when not
+ * necessary. It is on by default but it can be turned off via
+ * {@link #setMeasurementCacheEnabled(boolean)}.
+ *
+ * @return True if measurement cache is enabled, false otherwise.
+ *
+ * @see #setMeasurementCacheEnabled(boolean)
+ */
+ public boolean isMeasurementCacheEnabled() {
+ return mMeasurementCacheEnabled;
+ }
+
+ /**
+ * Sets whether RecyclerView should use its own measurement cache for the children. This is
+ * a more aggressive cache than the framework uses.
+ *
+ * @param measurementCacheEnabled True to enable the measurement cache, false otherwise.
+ *
+ * @see #isMeasurementCacheEnabled()
+ */
+ public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) {
+ mMeasurementCacheEnabled = measurementCacheEnabled;
+ }
+
+ private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) {
+ final int specMode = MeasureSpec.getMode(spec);
+ final int specSize = MeasureSpec.getSize(spec);
+ if (dimension > 0 && childSize != dimension) {
+ return false;
+ }
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ return true;
+ case MeasureSpec.AT_MOST:
+ return specSize >= childSize;
+ case MeasureSpec.EXACTLY:
+ return specSize == childSize;
+ }
+ return false;
+ }
+
+ /**
+ * Measure a child view using standard measurement policy, taking the padding
+ * of the parent RecyclerView, any added item decorations and the child margins
+ * into account.
+ *
+ * <p>If the RecyclerView can be scrolled in either dimension the caller may
+ * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
+ *
+ * @param child Child view to measure
+ * @param widthUsed Width in pixels currently consumed by other views, if relevant
+ * @param heightUsed Height in pixels currently consumed by other views, if relevant
+ */
+ public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+ widthUsed += insets.left + insets.right;
+ heightUsed += insets.top + insets.bottom;
+
+ final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
+ getPaddingLeft() + getPaddingRight()
+ + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
+ canScrollHorizontally());
+ final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
+ getPaddingTop() + getPaddingBottom()
+ + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
+ canScrollVertically());
+ if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
+ child.measure(widthSpec, heightSpec);
+ }
+ }
+
+ /**
+ * Calculate a MeasureSpec value for measuring a child view in one dimension.
+ *
+ * @param parentSize Size of the parent view where the child will be placed
+ * @param padding Total space currently consumed by other elements of the parent
+ * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
+ * Generally obtained from the child view's LayoutParams
+ * @param canScroll true if the parent RecyclerView can scroll in this dimension
+ *
+ * @return a MeasureSpec value for the child view
+ * @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)}
+ */
+ @Deprecated
+ public static int getChildMeasureSpec(int parentSize, int padding, int childDimension,
+ boolean canScroll) {
+ int size = Math.max(0, parentSize - padding);
+ int resultSize = 0;
+ int resultMode = 0;
+ if (canScroll) {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else {
+ // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
+ // instead using UNSPECIFIED.
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+ } else {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.MATCH_PARENT) {
+ resultSize = size;
+ // TODO this should be my spec.
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ }
+ }
+ return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+ }
+
+ /**
+ * Calculate a MeasureSpec value for measuring a child view in one dimension.
+ *
+ * @param parentSize Size of the parent view where the child will be placed
+ * @param parentMode The measurement spec mode of the parent
+ * @param padding Total space currently consumed by other elements of parent
+ * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
+ * Generally obtained from the child view's LayoutParams
+ * @param canScroll true if the parent RecyclerView can scroll in this dimension
+ *
+ * @return a MeasureSpec value for the child view
+ */
+ public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
+ int childDimension, boolean canScroll) {
+ int size = Math.max(0, parentSize - padding);
+ int resultSize = 0;
+ int resultMode = 0;
+ if (canScroll) {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.MATCH_PARENT) {
+ switch (parentMode) {
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.EXACTLY:
+ resultSize = size;
+ resultMode = parentMode;
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ break;
+ }
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+ } else {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.MATCH_PARENT) {
+ resultSize = size;
+ resultMode = parentMode;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ resultSize = size;
+ if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
+ resultMode = MeasureSpec.AT_MOST;
+ } else {
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+
+ }
+ }
+ //noinspection WrongConstant
+ return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+ }
+
+ /**
+ * Returns the measured width of the given child, plus the additional size of
+ * any insets applied by {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child view to query
+ * @return child's measured width plus <code>ItemDecoration</code> insets
+ *
+ * @see View#getMeasuredWidth()
+ */
+ public int getDecoratedMeasuredWidth(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getMeasuredWidth() + insets.left + insets.right;
+ }
+
+ /**
+ * Returns the measured height of the given child, plus the additional size of
+ * any insets applied by {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child view to query
+ * @return child's measured height plus <code>ItemDecoration</code> insets
+ *
+ * @see View#getMeasuredHeight()
+ */
+ public int getDecoratedMeasuredHeight(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getMeasuredHeight() + insets.top + insets.bottom;
+ }
+
+ /**
+ * Lay out the given child view within the RecyclerView using coordinates that
+ * include any current {@link ItemDecoration ItemDecorations}.
+ *
+ * <p>LayoutManagers should prefer working in sizes and coordinates that include
+ * item decoration insets whenever possible. This allows the LayoutManager to effectively
+ * ignore decoration insets within measurement and layout code. See the following
+ * methods:</p>
+ * <ul>
+ * <li>{@link #layoutDecoratedWithMargins(View, int, int, int, int)}</li>
+ * <li>{@link #getDecoratedBoundsWithMargins(View, Rect)}</li>
+ * <li>{@link #measureChild(View, int, int)}</li>
+ * <li>{@link #measureChildWithMargins(View, int, int)}</li>
+ * <li>{@link #getDecoratedLeft(View)}</li>
+ * <li>{@link #getDecoratedTop(View)}</li>
+ * <li>{@link #getDecoratedRight(View)}</li>
+ * <li>{@link #getDecoratedBottom(View)}</li>
+ * <li>{@link #getDecoratedMeasuredWidth(View)}</li>
+ * <li>{@link #getDecoratedMeasuredHeight(View)}</li>
+ * </ul>
+ *
+ * @param child Child to lay out
+ * @param left Left edge, with item decoration insets included
+ * @param top Top edge, with item decoration insets included
+ * @param right Right edge, with item decoration insets included
+ * @param bottom Bottom edge, with item decoration insets included
+ *
+ * @see View#layout(int, int, int, int)
+ * @see #layoutDecoratedWithMargins(View, int, int, int, int)
+ */
+ public void layoutDecorated(View child, int left, int top, int right, int bottom) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ child.layout(left + insets.left, top + insets.top, right - insets.right,
+ bottom - insets.bottom);
+ }
+
+ /**
+ * Lay out the given child view within the RecyclerView using coordinates that
+ * include any current {@link ItemDecoration ItemDecorations} and margins.
+ *
+ * <p>LayoutManagers should prefer working in sizes and coordinates that include
+ * item decoration insets whenever possible. This allows the LayoutManager to effectively
+ * ignore decoration insets within measurement and layout code. See the following
+ * methods:</p>
+ * <ul>
+ * <li>{@link #layoutDecorated(View, int, int, int, int)}</li>
+ * <li>{@link #measureChild(View, int, int)}</li>
+ * <li>{@link #measureChildWithMargins(View, int, int)}</li>
+ * <li>{@link #getDecoratedLeft(View)}</li>
+ * <li>{@link #getDecoratedTop(View)}</li>
+ * <li>{@link #getDecoratedRight(View)}</li>
+ * <li>{@link #getDecoratedBottom(View)}</li>
+ * <li>{@link #getDecoratedMeasuredWidth(View)}</li>
+ * <li>{@link #getDecoratedMeasuredHeight(View)}</li>
+ * </ul>
+ *
+ * @param child Child to lay out
+ * @param left Left edge, with item decoration insets and left margin included
+ * @param top Top edge, with item decoration insets and top margin included
+ * @param right Right edge, with item decoration insets and right margin included
+ * @param bottom Bottom edge, with item decoration insets and bottom margin included
+ *
+ * @see View#layout(int, int, int, int)
+ * @see #layoutDecorated(View, int, int, int, int)
+ */
+ public void layoutDecoratedWithMargins(View child, int left, int top, int right,
+ int bottom) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Rect insets = lp.mDecorInsets;
+ child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
+ right - insets.right - lp.rightMargin,
+ bottom - insets.bottom - lp.bottomMargin);
+ }
+
+ /**
+ * Calculates the bounding box of the View while taking into account its matrix changes
+ * (translation, scale etc) with respect to the RecyclerView.
+ * <p>
+ * If {@code includeDecorInsets} is {@code true}, they are applied first before applying
+ * the View's matrix so that the decor offsets also go through the same transformation.
+ *
+ * @param child The ItemView whose bounding box should be calculated.
+ * @param includeDecorInsets True if the decor insets should be included in the bounding box
+ * @param out The rectangle into which the output will be written.
+ */
+ public void getTransformedBoundingBox(View child, boolean includeDecorInsets, Rect out) {
+ if (includeDecorInsets) {
+ Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ out.set(-insets.left, -insets.top,
+ child.getWidth() + insets.right, child.getHeight() + insets.bottom);
+ } else {
+ out.set(0, 0, child.getWidth(), child.getHeight());
+ }
+
+ if (mRecyclerView != null) {
+ final Matrix childMatrix = child.getMatrix();
+ if (childMatrix != null && !childMatrix.isIdentity()) {
+ final RectF tempRectF = mRecyclerView.mTempRectF;
+ tempRectF.set(out);
+ childMatrix.mapRect(tempRectF);
+ out.set(
+ (int) Math.floor(tempRectF.left),
+ (int) Math.floor(tempRectF.top),
+ (int) Math.ceil(tempRectF.right),
+ (int) Math.ceil(tempRectF.bottom)
+ );
+ }
+ }
+ out.offset(child.getLeft(), child.getTop());
+ }
+
+ /**
+ * Returns the bounds of the view including its decoration and margins.
+ *
+ * @param view The view element to check
+ * @param outBounds A rect that will receive the bounds of the element including its
+ * decoration and margins.
+ */
+ public void getDecoratedBoundsWithMargins(View view, Rect outBounds) {
+ RecyclerView.getDecoratedBoundsWithMarginsInt(view, outBounds);
+ }
+
+ /**
+ * Returns the left edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child left edge with offsets applied
+ * @see #getLeftDecorationWidth(View)
+ */
+ public int getDecoratedLeft(View child) {
+ return child.getLeft() - getLeftDecorationWidth(child);
+ }
+
+ /**
+ * Returns the top edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child top edge with offsets applied
+ * @see #getTopDecorationHeight(View)
+ */
+ public int getDecoratedTop(View child) {
+ return child.getTop() - getTopDecorationHeight(child);
+ }
+
+ /**
+ * Returns the right edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child right edge with offsets applied
+ * @see #getRightDecorationWidth(View)
+ */
+ public int getDecoratedRight(View child) {
+ return child.getRight() + getRightDecorationWidth(child);
+ }
+
+ /**
+ * Returns the bottom edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child bottom edge with offsets applied
+ * @see #getBottomDecorationHeight(View)
+ */
+ public int getDecoratedBottom(View child) {
+ return child.getBottom() + getBottomDecorationHeight(child);
+ }
+
+ /**
+ * Calculates the item decor insets applied to the given child and updates the provided
+ * Rect instance with the inset values.
+ * <ul>
+ * <li>The Rect's left is set to the total width of left decorations.</li>
+ * <li>The Rect's top is set to the total height of top decorations.</li>
+ * <li>The Rect's right is set to the total width of right decorations.</li>
+ * <li>The Rect's bottom is set to total height of bottom decorations.</li>
+ * </ul>
+ * <p>
+ * Note that item decorations are automatically calculated when one of the LayoutManager's
+ * measure child methods is called. If you need to measure the child with custom specs via
+ * {@link View#measure(int, int)}, you can use this method to get decorations.
+ *
+ * @param child The child view whose decorations should be calculated
+ * @param outRect The Rect to hold result values
+ */
+ public void calculateItemDecorationsForChild(View child, Rect outRect) {
+ if (mRecyclerView == null) {
+ outRect.set(0, 0, 0, 0);
+ return;
+ }
+ Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+ outRect.set(insets);
+ }
+
+ /**
+ * Returns the total height of item decorations applied to child's top.
+ * <p>
+ * Note that this value is not updated until the View is measured or
+ * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+ *
+ * @param child Child to query
+ * @return The total height of item decorations applied to the child's top.
+ * @see #getDecoratedTop(View)
+ * @see #calculateItemDecorationsForChild(View, Rect)
+ */
+ public int getTopDecorationHeight(View child) {
+ return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top;
+ }
+
+ /**
+ * Returns the total height of item decorations applied to child's bottom.
+ * <p>
+ * Note that this value is not updated until the View is measured or
+ * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+ *
+ * @param child Child to query
+ * @return The total height of item decorations applied to the child's bottom.
+ * @see #getDecoratedBottom(View)
+ * @see #calculateItemDecorationsForChild(View, Rect)
+ */
+ public int getBottomDecorationHeight(View child) {
+ return ((LayoutParams) child.getLayoutParams()).mDecorInsets.bottom;
+ }
+
+ /**
+ * Returns the total width of item decorations applied to child's left.
+ * <p>
+ * Note that this value is not updated until the View is measured or
+ * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+ *
+ * @param child Child to query
+ * @return The total width of item decorations applied to the child's left.
+ * @see #getDecoratedLeft(View)
+ * @see #calculateItemDecorationsForChild(View, Rect)
+ */
+ public int getLeftDecorationWidth(View child) {
+ return ((LayoutParams) child.getLayoutParams()).mDecorInsets.left;
+ }
+
+ /**
+ * Returns the total width of item decorations applied to child's right.
+ * <p>
+ * Note that this value is not updated until the View is measured or
+ * {@link #calculateItemDecorationsForChild(View, Rect)} is called.
+ *
+ * @param child Child to query
+ * @return The total width of item decorations applied to the child's right.
+ * @see #getDecoratedRight(View)
+ * @see #calculateItemDecorationsForChild(View, Rect)
+ */
+ public int getRightDecorationWidth(View child) {
+ return ((LayoutParams) child.getLayoutParams()).mDecorInsets.right;
+ }
+
+ /**
+ * Called when searching for a focusable view in the given direction has failed
+ * for the current content of the RecyclerView.
+ *
+ * <p>This is the LayoutManager's opportunity to populate views in the given direction
+ * to fulfill the request if it can. The LayoutManager should attach and return
+ * the view to be focused. The default implementation returns null.</p>
+ *
+ * @param focused The currently focused view
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+ * or 0 for not applicable
+ * @param recycler The recycler to use for obtaining views for currently offscreen items
+ * @param state Transient state of RecyclerView
+ * @return The chosen view to be focused
+ */
+ @Nullable
+ public View onFocusSearchFailed(View focused, int direction, Recycler recycler,
+ State state) {
+ return null;
+ }
+
+ /**
+ * This method gives a LayoutManager an opportunity to intercept the initial focus search
+ * before the default behavior of {@link FocusFinder} is used. If this method returns
+ * null FocusFinder will attempt to find a focusable child view. If it fails
+ * then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)}
+ * will be called to give the LayoutManager an opportunity to add new views for items
+ * that did not have attached views representing them. The LayoutManager should not add
+ * or remove views from this method.
+ *
+ * @param focused The currently focused view
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+ * @return A descendant view to focus or null to fall back to default behavior.
+ * The default implementation returns null.
+ */
+ public View onInterceptFocusSearch(View focused, int direction) {
+ return null;
+ }
+
+ /**
+ * Called when a child of the RecyclerView wants a particular rectangle to be positioned
+ * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View,
+ * android.graphics.Rect, boolean)} for more details.
+ *
+ * <p>The base implementation will attempt to perform a standard programmatic scroll
+ * to bring the given rect into view, within the padded area of the RecyclerView.</p>
+ *
+ * @param child The direct child making the request.
+ * @param rect The rectangle in the child's coordinates the child
+ * wishes to be on the screen.
+ * @param immediate True to forbid animated or delayed scrolling,
+ * false otherwise
+ * @return Whether the group scrolled to handle the operation
+ */
+ public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
+ boolean immediate) {
+ final int parentLeft = getPaddingLeft();
+ final int parentTop = getPaddingTop();
+ final int parentRight = getWidth() - getPaddingRight();
+ final int parentBottom = getHeight() - getPaddingBottom();
+ final int childLeft = child.getLeft() + rect.left - child.getScrollX();
+ final int childTop = child.getTop() + rect.top - child.getScrollY();
+ final int childRight = childLeft + rect.width();
+ final int childBottom = childTop + rect.height();
+
+ final int offScreenLeft = Math.min(0, childLeft - parentLeft);
+ final int offScreenTop = Math.min(0, childTop - parentTop);
+ final int offScreenRight = Math.max(0, childRight - parentRight);
+ final int offScreenBottom = Math.max(0, childBottom - parentBottom);
+
+ // Favor the "start" layout direction over the end when bringing one side or the other
+ // of a large rect into view. If we decide to bring in end because start is already
+ // visible, limit the scroll such that start won't go out of bounds.
+ final int dx;
+ if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ dx = offScreenRight != 0 ? offScreenRight
+ : Math.max(offScreenLeft, childRight - parentRight);
+ } else {
+ dx = offScreenLeft != 0 ? offScreenLeft
+ : Math.min(childLeft - parentLeft, offScreenRight);
+ }
+
+ // Favor bringing the top into view over the bottom. If top is already visible and
+ // we should scroll to make bottom visible, make sure top does not go out of bounds.
+ final int dy = offScreenTop != 0 ? offScreenTop
+ : Math.min(childTop - parentTop, offScreenBottom);
+
+ if (dx != 0 || dy != 0) {
+ if (immediate) {
+ parent.scrollBy(dx, dy);
+ } else {
+ parent.smoothScrollBy(dx, dy);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)}
+ */
+ @Deprecated
+ public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
+ // eat the request if we are in the middle of a scroll or layout
+ return isSmoothScrolling() || parent.isComputingLayout();
+ }
+
+ /**
+ * Called when a descendant view of the RecyclerView requests focus.
+ *
+ * <p>A LayoutManager wishing to keep focused views aligned in a specific
+ * portion of the view may implement that behavior in an override of this method.</p>
+ *
+ * <p>If the LayoutManager executes different behavior that should override the default
+ * behavior of scrolling the focused child on screen instead of running alongside it,
+ * this method should return true.</p>
+ *
+ * @param parent The RecyclerView hosting this LayoutManager
+ * @param state Current state of RecyclerView
+ * @param child Direct child of the RecyclerView containing the newly focused view
+ * @param focused The newly focused view. This may be the same view as child or it may be
+ * null
+ * @return true if the default scroll behavior should be suppressed
+ */
+ public boolean onRequestChildFocus(RecyclerView parent, State state, View child,
+ View focused) {
+ return onRequestChildFocus(parent, child, focused);
+ }
+
+ /**
+ * Called if the RecyclerView this LayoutManager is bound to has a different adapter set.
+ * The LayoutManager may use this opportunity to clear caches and configure state such
+ * that it can relayout appropriately with the new data and potentially new view types.
+ *
+ * <p>The default implementation removes all currently attached views.</p>
+ *
+ * @param oldAdapter The previous adapter instance. Will be null if there was previously no
+ * adapter.
+ * @param newAdapter The new adapter instance. Might be null if
+ * {@link #setAdapter(RecyclerView.Adapter)} is called with {@code null}.
+ */
+ public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {
+ }
+
+ /**
+ * Called to populate focusable views within the RecyclerView.
+ *
+ * <p>The LayoutManager implementation should return <code>true</code> if the default
+ * behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be
+ * suppressed.</p>
+ *
+ * <p>The default implementation returns <code>false</code> to trigger RecyclerView
+ * to fall back to the default ViewGroup behavior.</p>
+ *
+ * @param recyclerView The RecyclerView hosting this LayoutManager
+ * @param views List of output views. This method should add valid focusable views
+ * to this list.
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+ * @param focusableMode The type of focusables to be added.
+ *
+ * @return true to suppress the default behavior, false to add default focusables after
+ * this method returns.
+ *
+ * @see #FOCUSABLES_ALL
+ * @see #FOCUSABLES_TOUCH_MODE
+ */
+ public boolean onAddFocusables(RecyclerView recyclerView, ArrayList<View> views,
+ int direction, int focusableMode) {
+ return false;
+ }
+
+ /**
+ * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving
+ * detailed information on what has actually changed.
+ *
+ * @param recyclerView
+ */
+ public void onItemsChanged(RecyclerView recyclerView) {
+ }
+
+ /**
+ * Called when items have been added to the adapter. The LayoutManager may choose to
+ * requestLayout if the inserted items would require refreshing the currently visible set
+ * of child views. (e.g. currently empty space would be filled by appended items, etc.)
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ */
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ }
+
+ /**
+ * Called when items have been removed from the adapter.
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ */
+ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+ }
+
+ /**
+ * Called when items have been changed in the adapter.
+ * To receive payload, override {@link #onItemsUpdated(RecyclerView, int, int, Object)}
+ * instead, then this callback will not be invoked.
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ */
+ public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
+ }
+
+ /**
+ * Called when items have been changed in the adapter and with optional payload.
+ * Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}.
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ * @param payload
+ */
+ public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
+ Object payload) {
+ onItemsUpdated(recyclerView, positionStart, itemCount);
+ }
+
+ /**
+ * Called when an item is moved withing the adapter.
+ * <p>
+ * Note that, an item may also change position in response to another ADD/REMOVE/MOVE
+ * operation. This callback is only called if and only if {@link Adapter#notifyItemMoved}
+ * is called.
+ *
+ * @param recyclerView
+ * @param from
+ * @param to
+ * @param itemCount
+ */
+ public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
+
+ }
+
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param state Current state of RecyclerView
+ * @return The horizontal extent of the scrollbar's thumb
+ * @see RecyclerView#computeHorizontalScrollExtent()
+ */
+ public int computeHorizontalScrollExtent(State state) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param state Current State of RecyclerView where you can find total item count
+ * @return The horizontal offset of the scrollbar's thumb
+ * @see RecyclerView#computeHorizontalScrollOffset()
+ */
+ public int computeHorizontalScrollOffset(State state) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeHorizontalScrollRange()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param state Current State of RecyclerView where you can find total item count
+ * @return The total horizontal range represented by the vertical scrollbar
+ * @see RecyclerView#computeHorizontalScrollRange()
+ */
+ public int computeHorizontalScrollRange(State state) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeVerticalScrollExtent()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param state Current state of RecyclerView
+ * @return The vertical extent of the scrollbar's thumb
+ * @see RecyclerView#computeVerticalScrollExtent()
+ */
+ public int computeVerticalScrollExtent(State state) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeVerticalScrollOffset()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param state Current State of RecyclerView where you can find total item count
+ * @return The vertical offset of the scrollbar's thumb
+ * @see RecyclerView#computeVerticalScrollOffset()
+ */
+ public int computeVerticalScrollOffset(State state) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeVerticalScrollRange()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param state Current State of RecyclerView where you can find total item count
+ * @return The total vertical range represented by the vertical scrollbar
+ * @see RecyclerView#computeVerticalScrollRange()
+ */
+ public int computeVerticalScrollRange(State state) {
+ return 0;
+ }
+
+ /**
+ * Measure the attached RecyclerView. Implementations must call
+ * {@link #setMeasuredDimension(int, int)} before returning.
+ *
+ * <p>The default implementation will handle EXACTLY measurements and respect
+ * the minimum width and height properties of the host RecyclerView if measured
+ * as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView
+ * will consume all available space.</p>
+ *
+ * @param recycler Recycler
+ * @param state Transient state of RecyclerView
+ * @param widthSpec Width {@link android.view.View.MeasureSpec}
+ * @param heightSpec Height {@link android.view.View.MeasureSpec}
+ */
+ public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
+ mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
+ }
+
+ /**
+ * {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the
+ * host RecyclerView.
+ *
+ * @param widthSize Measured width
+ * @param heightSize Measured height
+ */
+ public void setMeasuredDimension(int widthSize, int heightSize) {
+ mRecyclerView.setMeasuredDimension(widthSize, heightSize);
+ }
+
+ /**
+ * @return The host RecyclerView's {@link View#getMinimumWidth()}
+ */
+ public int getMinimumWidth() {
+ return mRecyclerView.getMinimumWidth();
+ }
+
+ /**
+ * @return The host RecyclerView's {@link View#getMinimumHeight()}
+ */
+ public int getMinimumHeight() {
+ return mRecyclerView.getMinimumHeight();
+ }
+ /**
+ * <p>Called when the LayoutManager should save its state. This is a good time to save your
+ * scroll position, configuration and anything else that may be required to restore the same
+ * layout state if the LayoutManager is recreated.</p>
+ * <p>RecyclerView does NOT verify if the LayoutManager has changed between state save and
+ * restore. This will let you share information between your LayoutManagers but it is also
+ * your responsibility to make sure they use the same parcelable class.</p>
+ *
+ * @return Necessary information for LayoutManager to be able to restore its state
+ */
+ public Parcelable onSaveInstanceState() {
+ return null;
+ }
+
+
+ public void onRestoreInstanceState(Parcelable state) {
+
+ }
+
+ void stopSmoothScroller() {
+ if (mSmoothScroller != null) {
+ mSmoothScroller.stop();
+ }
+ }
+
+ private void onSmoothScrollerStopped(SmoothScroller smoothScroller) {
+ if (mSmoothScroller == smoothScroller) {
+ mSmoothScroller = null;
+ }
+ }
+
+ /**
+ * RecyclerView calls this method to notify LayoutManager that scroll state has changed.
+ *
+ * @param state The new scroll state for RecyclerView
+ */
+ public void onScrollStateChanged(int state) {
+ }
+
+ /**
+ * Removes all views and recycles them using the given recycler.
+ * <p>
+ * If you want to clean cached views as well, you should call {@link Recycler#clear()} too.
+ * <p>
+ * If a View is marked as "ignored", it is not removed nor recycled.
+ *
+ * @param recycler Recycler to use to recycle children
+ * @see #removeAndRecycleView(View, Recycler)
+ * @see #removeAndRecycleViewAt(int, Recycler)
+ * @see #ignoreView(View)
+ */
+ public void removeAndRecycleAllViews(Recycler recycler) {
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ final View view = getChildAt(i);
+ if (!getChildViewHolderInt(view).shouldIgnore()) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ }
+ }
+
+ // called by accessibility delegate
+ void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info);
+ }
+
+ /**
+ * Called by the AccessibilityDelegate when the information about the current layout should
+ * be populated.
+ * <p>
+ * Default implementation adds a {@link
+ * android.view.accessibility.AccessibilityNodeInfo.CollectionInfo}.
+ * <p>
+ * You should override
+ * {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)},
+ * {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)},
+ * {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and
+ * {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for
+ * more accurate accessibility information.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @param info The info that should be filled by the LayoutManager
+ * @see View#onInitializeAccessibilityNodeInfo(
+ *android.view.accessibility.AccessibilityNodeInfo)
+ * @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)
+ * @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)
+ * @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)
+ * @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)
+ */
+ public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
+ AccessibilityNodeInfo info) {
+ if (mRecyclerView.canScrollVertically(-1)
+ || mRecyclerView.canScrollHorizontally(-1)) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ info.setScrollable(true);
+ }
+ if (mRecyclerView.canScrollVertically(1)
+ || mRecyclerView.canScrollHorizontally(1)) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ info.setScrollable(true);
+ }
+ final AccessibilityNodeInfo.CollectionInfo collectionInfo =
+ AccessibilityNodeInfo.CollectionInfo
+ .obtain(getRowCountForAccessibility(recycler, state),
+ getColumnCountForAccessibility(recycler, state),
+ isLayoutHierarchical(recycler, state),
+ getSelectionModeForAccessibility(recycler, state));
+ info.setCollectionInfo(collectionInfo);
+ }
+
+ // called by accessibility delegate
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ onInitializeAccessibilityEvent(mRecyclerView.mRecycler, mRecyclerView.mState, event);
+ }
+
+ /**
+ * Called by the accessibility delegate to initialize an accessibility event.
+ * <p>
+ * Default implementation adds item count and scroll information to the event.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @param event The event instance to initialize
+ * @see View#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent)
+ */
+ public void onInitializeAccessibilityEvent(Recycler recycler, State state,
+ AccessibilityEvent event) {
+ if (mRecyclerView == null || event == null) {
+ return;
+ }
+ event.setScrollable(mRecyclerView.canScrollVertically(1)
+ || mRecyclerView.canScrollVertically(-1)
+ || mRecyclerView.canScrollHorizontally(-1)
+ || mRecyclerView.canScrollHorizontally(1));
+
+ if (mRecyclerView.mAdapter != null) {
+ event.setItemCount(mRecyclerView.mAdapter.getItemCount());
+ }
+ }
+
+ // called by accessibility delegate
+ void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfo info) {
+ final ViewHolder vh = getChildViewHolderInt(host);
+ // avoid trying to create accessibility node info for removed children
+ if (vh != null && !vh.isRemoved() && !mChildHelper.isHidden(vh.itemView)) {
+ onInitializeAccessibilityNodeInfoForItem(mRecyclerView.mRecycler,
+ mRecyclerView.mState, host, info);
+ }
+ }
+
+ /**
+ * Called by the AccessibilityDelegate when the accessibility information for a specific
+ * item should be populated.
+ * <p>
+ * Default implementation adds basic positioning information about the item.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @param host The child for which accessibility node info should be populated
+ * @param info The info to fill out about the item
+ * @see android.widget.AbsListView#onInitializeAccessibilityNodeInfoForItem(View, int,
+ * android.view.accessibility.AccessibilityNodeInfo)
+ */
+ public void onInitializeAccessibilityNodeInfoForItem(Recycler recycler, State state,
+ View host, AccessibilityNodeInfo info) {
+ int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0;
+ int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0;
+ final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
+ AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndexGuess, 1,
+ columnIndexGuess, 1, false, false);
+ info.setCollectionItemInfo(itemInfo);
+ }
+
+ /**
+ * A LayoutManager can call this method to force RecyclerView to run simple animations in
+ * the next layout pass, even if there is not any trigger to do so. (e.g. adapter data
+ * change).
+ * <p>
+ * Note that, calling this method will not guarantee that RecyclerView will run animations
+ * at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will
+ * not run any animations but will still clear this flag after the layout is complete.
+ *
+ */
+ public void requestSimpleAnimationsInNextLayout() {
+ mRequestedSimpleAnimations = true;
+ }
+
+ /**
+ * Returns the selection mode for accessibility. Should be
+ * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE},
+ * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_SINGLE} or
+ * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_MULTIPLE}.
+ * <p>
+ * Default implementation returns
+ * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @return Selection mode for accessibility. Default implementation returns
+ * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}.
+ */
+ public int getSelectionModeForAccessibility(Recycler recycler, State state) {
+ return AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE;
+ }
+
+ /**
+ * Returns the number of rows for accessibility.
+ * <p>
+ * Default implementation returns the number of items in the adapter if LayoutManager
+ * supports vertical scrolling or 1 if LayoutManager does not support vertical
+ * scrolling.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @return The number of rows in LayoutManager for accessibility.
+ */
+ public int getRowCountForAccessibility(Recycler recycler, State state) {
+ if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
+ return 1;
+ }
+ return canScrollVertically() ? mRecyclerView.mAdapter.getItemCount() : 1;
+ }
+
+ /**
+ * Returns the number of columns for accessibility.
+ * <p>
+ * Default implementation returns the number of items in the adapter if LayoutManager
+ * supports horizontal scrolling or 1 if LayoutManager does not support horizontal
+ * scrolling.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @return The number of rows in LayoutManager for accessibility.
+ */
+ public int getColumnCountForAccessibility(Recycler recycler, State state) {
+ if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
+ return 1;
+ }
+ return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 1;
+ }
+
+ /**
+ * Returns whether layout is hierarchical or not to be used for accessibility.
+ * <p>
+ * Default implementation returns false.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @return True if layout is hierarchical.
+ */
+ public boolean isLayoutHierarchical(Recycler recycler, State state) {
+ return false;
+ }
+
+ // called by accessibility delegate
+ boolean performAccessibilityAction(int action, Bundle args) {
+ return performAccessibilityAction(mRecyclerView.mRecycler, mRecyclerView.mState,
+ action, args);
+ }
+
+ /**
+ * Called by AccessibilityDelegate when an action is requested from the RecyclerView.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @param action The action to perform
+ * @param args Optional action arguments
+ * @see View#performAccessibilityAction(int, android.os.Bundle)
+ */
+ public boolean performAccessibilityAction(Recycler recycler, State state, int action,
+ Bundle args) {
+ if (mRecyclerView == null) {
+ return false;
+ }
+ int vScroll = 0, hScroll = 0;
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ if (mRecyclerView.canScrollVertically(-1)) {
+ vScroll = -(getHeight() - getPaddingTop() - getPaddingBottom());
+ }
+ if (mRecyclerView.canScrollHorizontally(-1)) {
+ hScroll = -(getWidth() - getPaddingLeft() - getPaddingRight());
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ if (mRecyclerView.canScrollVertically(1)) {
+ vScroll = getHeight() - getPaddingTop() - getPaddingBottom();
+ }
+ if (mRecyclerView.canScrollHorizontally(1)) {
+ hScroll = getWidth() - getPaddingLeft() - getPaddingRight();
+ }
+ break;
+ }
+ if (vScroll == 0 && hScroll == 0) {
+ return false;
+ }
+ mRecyclerView.smoothScrollBy(hScroll, vScroll);
+ return true;
+ }
+
+ // called by accessibility delegate
+ boolean performAccessibilityActionForItem(View view, int action, Bundle args) {
+ return performAccessibilityActionForItem(mRecyclerView.mRecycler, mRecyclerView.mState,
+ view, action, args);
+ }
+
+ /**
+ * Called by AccessibilityDelegate when an accessibility action is requested on one of the
+ * children of LayoutManager.
+ * <p>
+ * Default implementation does not do anything.
+ *
+ * @param recycler The Recycler that can be used to convert view positions into adapter
+ * positions
+ * @param state The current state of RecyclerView
+ * @param view The child view on which the action is performed
+ * @param action The action to perform
+ * @param args Optional action arguments
+ * @return true if action is handled
+ * @see View#performAccessibilityAction(int, android.os.Bundle)
+ */
+ public boolean performAccessibilityActionForItem(Recycler recycler, State state, View view,
+ int action, Bundle args) {
+ return false;
+ }
+
+ /**
+ * Parse the xml attributes to get the most common properties used by layout managers.
+ *
+ * @return an object containing the properties as specified in the attrs.
+ */
+ public static Properties getProperties(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ Properties properties = new Properties();
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
+ defStyleAttr, defStyleRes);
+ properties.orientation = a.getInt(R.styleable.RecyclerView_orientation, VERTICAL);
+ properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1);
+ properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false);
+ properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false);
+ a.recycle();
+ return properties;
+ }
+
+ void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
+ setMeasureSpecs(
+ MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY)
+ );
+ }
+
+ /**
+ * Internal API to allow LayoutManagers to be measured twice.
+ * <p>
+ * This is not public because LayoutManagers should be able to handle their layouts in one
+ * pass but it is very convenient to make existing LayoutManagers support wrapping content
+ * when both orientations are undefined.
+ * <p>
+ * This API will be removed after default LayoutManagers properly implement wrap content in
+ * non-scroll orientation.
+ */
+ boolean shouldMeasureTwice() {
+ return false;
+ }
+
+ boolean hasFlexibleChildInBothOrientations() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final ViewGroup.LayoutParams lp = child.getLayoutParams();
+ if (lp.width < 0 && lp.height < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Some general properties that a LayoutManager may want to use.
+ */
+ public static class Properties {
+ /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation */
+ public int orientation;
+ /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount */
+ public int spanCount;
+ /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout */
+ public boolean reverseLayout;
+ /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd */
+ public boolean stackFromEnd;
+ }
+ }
+
+ /**
+ * An ItemDecoration allows the application to add a special drawing and layout offset
+ * to specific item views from the adapter's data set. This can be useful for drawing dividers
+ * between items, highlights, visual grouping boundaries and more.
+ *
+ * <p>All ItemDecorations are drawn in the order they were added, before the item
+ * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
+ * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
+ * RecyclerView.State)}.</p>
+ */
+ public abstract static class ItemDecoration {
+ /**
+ * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
+ * Any content drawn by this method will be drawn before the item views are drawn,
+ * and will thus appear underneath the views.
+ *
+ * @param c Canvas to draw into
+ * @param parent RecyclerView this ItemDecoration is drawing into
+ * @param state The current state of RecyclerView
+ */
+ public void onDraw(Canvas c, RecyclerView parent, State state) {
+ onDraw(c, parent);
+ }
+
+ /**
+ * @deprecated
+ * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
+ */
+ @Deprecated
+ public void onDraw(Canvas c, RecyclerView parent) {
+ }
+
+ /**
+ * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
+ * Any content drawn by this method will be drawn after the item views are drawn
+ * and will thus appear over the views.
+ *
+ * @param c Canvas to draw into
+ * @param parent RecyclerView this ItemDecoration is drawing into
+ * @param state The current state of RecyclerView.
+ */
+ public void onDrawOver(Canvas c, RecyclerView parent, State state) {
+ onDrawOver(c, parent);
+ }
+
+ /**
+ * @deprecated
+ * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
+ */
+ @Deprecated
+ public void onDrawOver(Canvas c, RecyclerView parent) {
+ }
+
+
+ /**
+ * @deprecated
+ * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
+ */
+ @Deprecated
+ public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
+ outRect.set(0, 0, 0, 0);
+ }
+
+ /**
+ * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
+ * the number of pixels that the item view should be inset by, similar to padding or margin.
+ * The default implementation sets the bounds of outRect to 0 and returns.
+ *
+ * <p>
+ * If this ItemDecoration does not affect the positioning of item views, it should set
+ * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
+ * before returning.
+ *
+ * <p>
+ * If you need to access Adapter for additional data, you can call
+ * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
+ * View.
+ *
+ * @param outRect Rect to receive the output.
+ * @param view The child view to decorate
+ * @param parent RecyclerView this ItemDecoration is decorating
+ * @param state The current state of RecyclerView.
+ */
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
+ getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
+ parent);
+ }
+ }
+
+ /**
+ * An OnItemTouchListener allows the application to intercept touch events in progress at the
+ * view hierarchy level of the RecyclerView before those touch events are considered for
+ * RecyclerView's own scrolling behavior.
+ *
+ * <p>This can be useful for applications that wish to implement various forms of gestural
+ * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept
+ * a touch interaction already in progress even if the RecyclerView is already handling that
+ * gesture stream itself for the purposes of scrolling.</p>
+ *
+ * @see SimpleOnItemTouchListener
+ */
+ public interface OnItemTouchListener {
+ /**
+ * Silently observe and/or take over touch events sent to the RecyclerView
+ * before they are handled by either the RecyclerView itself or its child views.
+ *
+ * <p>The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run
+ * in the order in which each listener was added, before any other touch processing
+ * by the RecyclerView itself or child views occurs.</p>
+ *
+ * @param e MotionEvent describing the touch event. All coordinates are in
+ * the RecyclerView's coordinate system.
+ * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false
+ * to continue with the current behavior and continue observing future events in
+ * the gesture.
+ */
+ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);
+
+ /**
+ * Process a touch event as part of a gesture that was claimed by returning true from
+ * a previous call to {@link #onInterceptTouchEvent}.
+ *
+ * @param e MotionEvent describing the touch event. All coordinates are in
+ * the RecyclerView's coordinate system.
+ */
+ void onTouchEvent(RecyclerView rv, MotionEvent e);
+
+ /**
+ * Called when a child of RecyclerView does not want RecyclerView and its ancestors to
+ * intercept touch events with
+ * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
+ *
+ * @param disallowIntercept True if the child does not want the parent to
+ * intercept touch events.
+ * @see ViewParent#requestDisallowInterceptTouchEvent(boolean)
+ */
+ void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
+ }
+
+ /**
+ * An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies
+ * and default return values.
+ * <p>
+ * You may prefer to extend this class if you don't need to override all methods. Another
+ * benefit of using this class is future compatibility. As the interface may change, we'll
+ * always provide a default implementation on this class so that your code won't break when
+ * you update to a new version of the support library.
+ */
+ public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+ }
+
+
+ /**
+ * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event
+ * has occurred on that RecyclerView.
+ * <p>
+ * @see RecyclerView#addOnScrollListener(OnScrollListener)
+ * @see RecyclerView#clearOnChildAttachStateChangeListeners()
+ *
+ */
+ public abstract static class OnScrollListener {
+ /**
+ * Callback method to be invoked when RecyclerView's scroll state changes.
+ *
+ * @param recyclerView The RecyclerView whose scroll state has changed.
+ * @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
+ * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
+ */
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState){}
+
+ /**
+ * Callback method to be invoked when the RecyclerView has been scrolled. This will be
+ * called after the scroll has completed.
+ * <p>
+ * This callback will also be called if visible item range changes after a layout
+ * calculation. In that case, dx and dy will be 0.
+ *
+ * @param recyclerView The RecyclerView which scrolled.
+ * @param dx The amount of horizontal scroll.
+ * @param dy The amount of vertical scroll.
+ */
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
+ }
+
+ /**
+ * A RecyclerListener can be set on a RecyclerView to receive messages whenever
+ * a view is recycled.
+ *
+ * @see RecyclerView#setRecyclerListener(RecyclerListener)
+ */
+ public interface RecyclerListener {
+
+ /**
+ * This method is called whenever the view in the ViewHolder is recycled.
+ *
+ * RecyclerView calls this method right before clearing ViewHolder's internal data and
+ * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information
+ * before being recycled, you can call {@link ViewHolder#getAdapterPosition()} to get
+ * its adapter position.
+ *
+ * @param holder The ViewHolder containing the view that was recycled
+ */
+ void onViewRecycled(ViewHolder holder);
+ }
+
+ /**
+ * A Listener interface that can be attached to a RecylcerView to get notified
+ * whenever a ViewHolder is attached to or detached from RecyclerView.
+ */
+ public interface OnChildAttachStateChangeListener {
+
+ /**
+ * Called when a view is attached to the RecyclerView.
+ *
+ * @param view The View which is attached to the RecyclerView
+ */
+ void onChildViewAttachedToWindow(View view);
+
+ /**
+ * Called when a view is detached from RecyclerView.
+ *
+ * @param view The View which is being detached from the RecyclerView
+ */
+ void onChildViewDetachedFromWindow(View view);
+ }
+
+ /**
+ * A ViewHolder describes an item view and metadata about its place within the RecyclerView.
+ *
+ * <p>{@link Adapter} implementations should subclass ViewHolder and add fields for caching
+ * potentially expensive {@link View#findViewById(int)} results.</p>
+ *
+ * <p>While {@link LayoutParams} belong to the {@link LayoutManager},
+ * {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use
+ * their own custom ViewHolder implementations to store data that makes binding view contents
+ * easier. Implementations should assume that individual item views will hold strong references
+ * to <code>ViewHolder</code> objects and that <code>RecyclerView</code> instances may hold
+ * strong references to extra off-screen item views for caching purposes</p>
+ */
+ public abstract static class ViewHolder {
+ public final View itemView;
+ WeakReference<RecyclerView> mNestedRecyclerView;
+ int mPosition = NO_POSITION;
+ int mOldPosition = NO_POSITION;
+ long mItemId = NO_ID;
+ int mItemViewType = INVALID_TYPE;
+ int mPreLayoutPosition = NO_POSITION;
+
+ // The item that this holder is shadowing during an item change event/animation
+ ViewHolder mShadowedHolder = null;
+ // The item that is shadowing this holder during an item change event/animation
+ ViewHolder mShadowingHolder = null;
+
+ /**
+ * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
+ * are all valid.
+ */
+ static final int FLAG_BOUND = 1 << 0;
+
+ /**
+ * The data this ViewHolder's view reflects is stale and needs to be rebound
+ * by the adapter. mPosition and mItemId are consistent.
+ */
+ static final int FLAG_UPDATE = 1 << 1;
+
+ /**
+ * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
+ * are not to be trusted and may no longer match the item view type.
+ * This ViewHolder must be fully rebound to different data.
+ */
+ static final int FLAG_INVALID = 1 << 2;
+
+ /**
+ * This ViewHolder points at data that represents an item previously removed from the
+ * data set. Its view may still be used for things like outgoing animations.
+ */
+ static final int FLAG_REMOVED = 1 << 3;
+
+ /**
+ * This ViewHolder should not be recycled. This flag is set via setIsRecyclable()
+ * and is intended to keep views around during animations.
+ */
+ static final int FLAG_NOT_RECYCLABLE = 1 << 4;
+
+ /**
+ * This ViewHolder is returned from scrap which means we are expecting an addView call
+ * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until
+ * the end of the layout pass and then recycled by RecyclerView if it is not added back to
+ * the RecyclerView.
+ */
+ static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5;
+
+ /**
+ * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove
+ * it unless LayoutManager is replaced.
+ * It is still fully visible to the LayoutManager.
+ */
+ static final int FLAG_IGNORE = 1 << 7;
+
+ /**
+ * When the View is detached form the parent, we set this flag so that we can take correct
+ * action when we need to remove it or add it back.
+ */
+ static final int FLAG_TMP_DETACHED = 1 << 8;
+
+ /**
+ * Set when we can no longer determine the adapter position of this ViewHolder until it is
+ * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is
+ * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon
+ * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is
+ * re-calculated.
+ */
+ static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9;
+
+ /**
+ * Set when a addChangePayload(null) is called
+ */
+ static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10;
+
+ /**
+ * Used by ItemAnimator when a ViewHolder's position changes
+ */
+ static final int FLAG_MOVED = 1 << 11;
+
+ /**
+ * Used by ItemAnimator when a ViewHolder appears in pre-layout
+ */
+ static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;
+
+ static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1;
+
+ /**
+ * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
+ * hidden list (as if it was scrap) without being recycled in between.
+ *
+ * When a ViewHolder is hidden, there are 2 paths it can be re-used:
+ * a) Animation ends, view is recycled and used from the recycle pool.
+ * b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
+ *
+ * This flag is used to represent "case b" where the ViewHolder is reused without being
+ * recycled (thus "bounced" from the hidden list). This state requires special handling
+ * because the ViewHolder must be added to pre layout maps for animations as if it was
+ * already there.
+ */
+ static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
+
+ private int mFlags;
+
+ private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
+
+ List<Object> mPayloads = null;
+ List<Object> mUnmodifiedPayloads = null;
+
+ private int mIsRecyclableCount = 0;
+
+ // If non-null, view is currently considered scrap and may be reused for other data by the
+ // scrap container.
+ private Recycler mScrapContainer = null;
+ // Keeps whether this ViewHolder lives in Change scrap or Attached scrap
+ private boolean mInChangeScrap = false;
+
+ // Saves isImportantForAccessibility value for the view item while it's in hidden state and
+ // marked as unimportant for accessibility.
+ private int mWasImportantForAccessibilityBeforeHidden =
+ View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+ // set if we defer the accessibility state change of the view holder
+ @VisibleForTesting
+ int mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
+
+ /**
+ * Is set when VH is bound from the adapter and cleaned right before it is sent to
+ * {@link RecycledViewPool}.
+ */
+ RecyclerView mOwnerRecyclerView;
+
+ public ViewHolder(View itemView) {
+ if (itemView == null) {
+ throw new IllegalArgumentException("itemView may not be null");
+ }
+ this.itemView = itemView;
+ }
+
+ void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
+ addFlags(ViewHolder.FLAG_REMOVED);
+ offsetPosition(offset, applyToPreLayout);
+ mPosition = mNewPosition;
+ }
+
+ void offsetPosition(int offset, boolean applyToPreLayout) {
+ if (mOldPosition == NO_POSITION) {
+ mOldPosition = mPosition;
+ }
+ if (mPreLayoutPosition == NO_POSITION) {
+ mPreLayoutPosition = mPosition;
+ }
+ if (applyToPreLayout) {
+ mPreLayoutPosition += offset;
+ }
+ mPosition += offset;
+ if (itemView.getLayoutParams() != null) {
+ ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true;
+ }
+ }
+
+ void clearOldPosition() {
+ mOldPosition = NO_POSITION;
+ mPreLayoutPosition = NO_POSITION;
+ }
+
+ void saveOldPosition() {
+ if (mOldPosition == NO_POSITION) {
+ mOldPosition = mPosition;
+ }
+ }
+
+ boolean shouldIgnore() {
+ return (mFlags & FLAG_IGNORE) != 0;
+ }
+
+ /**
+ * @deprecated This method is deprecated because its meaning is ambiguous due to the async
+ * handling of adapter updates. Please use {@link #getLayoutPosition()} or
+ * {@link #getAdapterPosition()} depending on your use case.
+ *
+ * @see #getLayoutPosition()
+ * @see #getAdapterPosition()
+ */
+ @Deprecated
+ public final int getPosition() {
+ return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
+ }
+
+ /**
+ * Returns the position of the ViewHolder in terms of the latest layout pass.
+ * <p>
+ * This position is mostly used by RecyclerView components to be consistent while
+ * RecyclerView lazily processes adapter updates.
+ * <p>
+ * For performance and animation reasons, RecyclerView batches all adapter updates until the
+ * next layout pass. This may cause mismatches between the Adapter position of the item and
+ * the position it had in the latest layout calculations.
+ * <p>
+ * LayoutManagers should always call this method while doing calculations based on item
+ * positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State},
+ * {@link RecyclerView.Recycler} that receive a position expect it to be the layout position
+ * of the item.
+ * <p>
+ * If LayoutManager needs to call an external method that requires the adapter position of
+ * the item, it can use {@link #getAdapterPosition()} or
+ * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}.
+ *
+ * @return Returns the adapter position of the ViewHolder in the latest layout pass.
+ * @see #getAdapterPosition()
+ */
+ public final int getLayoutPosition() {
+ return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
+ }
+
+ /**
+ * Returns the Adapter position of the item represented by this ViewHolder.
+ * <p>
+ * Note that this might be different than the {@link #getLayoutPosition()} if there are
+ * pending adapter updates but a new layout pass has not happened yet.
+ * <p>
+ * RecyclerView does not handle any adapter updates until the next layout traversal. This
+ * may create temporary inconsistencies between what user sees on the screen and what
+ * adapter contents have. This inconsistency is not important since it will be less than
+ * 16ms but it might be a problem if you want to use ViewHolder position to access the
+ * adapter. Sometimes, you may need to get the exact adapter position to do
+ * some actions in response to user events. In that case, you should use this method which
+ * will calculate the Adapter position of the ViewHolder.
+ * <p>
+ * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the
+ * next layout pass, the return value of this method will be {@link #NO_POSITION}.
+ *
+ * @return The adapter position of the item if it still exists in the adapter.
+ * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter,
+ * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last
+ * layout pass or the ViewHolder has already been recycled.
+ */
+ public final int getAdapterPosition() {
+ if (mOwnerRecyclerView == null) {
+ return NO_POSITION;
+ }
+ return mOwnerRecyclerView.getAdapterPositionFor(this);
+ }
+
+ /**
+ * When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders
+ * to perform animations.
+ * <p>
+ * If a ViewHolder was laid out in the previous onLayout call, old position will keep its
+ * adapter index in the previous layout.
+ *
+ * @return The previous adapter index of the Item represented by this ViewHolder or
+ * {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is
+ * complete).
+ */
+ public final int getOldPosition() {
+ return mOldPosition;
+ }
+
+ /**
+ * Returns The itemId represented by this ViewHolder.
+ *
+ * @return The item's id if adapter has stable ids, {@link RecyclerView#NO_ID}
+ * otherwise
+ */
+ public final long getItemId() {
+ return mItemId;
+ }
+
+ /**
+ * @return The view type of this ViewHolder.
+ */
+ public final int getItemViewType() {
+ return mItemViewType;
+ }
+
+ boolean isScrap() {
+ return mScrapContainer != null;
+ }
+
+ void unScrap() {
+ mScrapContainer.unscrapView(this);
+ }
+
+ boolean wasReturnedFromScrap() {
+ return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0;
+ }
+
+ void clearReturnedFromScrapFlag() {
+ mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP;
+ }
+
+ void clearTmpDetachFlag() {
+ mFlags = mFlags & ~FLAG_TMP_DETACHED;
+ }
+
+ void stopIgnoring() {
+ mFlags = mFlags & ~FLAG_IGNORE;
+ }
+
+ void setScrapContainer(Recycler recycler, boolean isChangeScrap) {
+ mScrapContainer = recycler;
+ mInChangeScrap = isChangeScrap;
+ }
+
+ boolean isInvalid() {
+ return (mFlags & FLAG_INVALID) != 0;
+ }
+
+ boolean needsUpdate() {
+ return (mFlags & FLAG_UPDATE) != 0;
+ }
+
+ boolean isBound() {
+ return (mFlags & FLAG_BOUND) != 0;
+ }
+
+ boolean isRemoved() {
+ return (mFlags & FLAG_REMOVED) != 0;
+ }
+
+ boolean hasAnyOfTheFlags(int flags) {
+ return (mFlags & flags) != 0;
+ }
+
+ boolean isTmpDetached() {
+ return (mFlags & FLAG_TMP_DETACHED) != 0;
+ }
+
+ boolean isAdapterPositionUnknown() {
+ return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid();
+ }
+
+ void setFlags(int flags, int mask) {
+ mFlags = (mFlags & ~mask) | (flags & mask);
+ }
+
+ void addFlags(int flags) {
+ mFlags |= flags;
+ }
+
+ void addChangePayload(Object payload) {
+ if (payload == null) {
+ addFlags(FLAG_ADAPTER_FULLUPDATE);
+ } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
+ createPayloadsIfNeeded();
+ mPayloads.add(payload);
+ }
+ }
+
+ private void createPayloadsIfNeeded() {
+ if (mPayloads == null) {
+ mPayloads = new ArrayList<Object>();
+ mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads);
+ }
+ }
+
+ void clearPayload() {
+ if (mPayloads != null) {
+ mPayloads.clear();
+ }
+ mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE;
+ }
+
+ List<Object> getUnmodifiedPayloads() {
+ if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
+ if (mPayloads == null || mPayloads.size() == 0) {
+ // Initial state, no update being called.
+ return FULLUPDATE_PAYLOADS;
+ }
+ // there are none-null payloads
+ return mUnmodifiedPayloads;
+ } else {
+ // a full update has been called.
+ return FULLUPDATE_PAYLOADS;
+ }
+ }
+
+ void resetInternal() {
+ mFlags = 0;
+ mPosition = NO_POSITION;
+ mOldPosition = NO_POSITION;
+ mItemId = NO_ID;
+ mPreLayoutPosition = NO_POSITION;
+ mIsRecyclableCount = 0;
+ mShadowedHolder = null;
+ mShadowingHolder = null;
+ clearPayload();
+ mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+ mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
+ clearNestedRecyclerViewIfNotNested(this);
+ }
+
+ /**
+ * Called when the child view enters the hidden state
+ */
+ private void onEnteredHiddenState(RecyclerView parent) {
+ // While the view item is in hidden state, make it invisible for the accessibility.
+ mWasImportantForAccessibilityBeforeHidden =
+ itemView.getImportantForAccessibility();
+ parent.setChildImportantForAccessibilityInternal(this,
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ }
+
+ /**
+ * Called when the child view leaves the hidden state
+ */
+ private void onLeftHiddenState(RecyclerView parent) {
+ parent.setChildImportantForAccessibilityInternal(this,
+ mWasImportantForAccessibilityBeforeHidden);
+ mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ViewHolder{"
+ + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId
+ + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition);
+ if (isScrap()) {
+ sb.append(" scrap ")
+ .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]");
+ }
+ if (isInvalid()) sb.append(" invalid");
+ if (!isBound()) sb.append(" unbound");
+ if (needsUpdate()) sb.append(" update");
+ if (isRemoved()) sb.append(" removed");
+ if (shouldIgnore()) sb.append(" ignored");
+ if (isTmpDetached()) sb.append(" tmpDetached");
+ if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")");
+ if (isAdapterPositionUnknown()) sb.append(" undefined adapter position");
+
+ if (itemView.getParent() == null) sb.append(" no parent");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Informs the recycler whether this item can be recycled. Views which are not
+ * recyclable will not be reused for other items until setIsRecyclable() is
+ * later set to true. Calls to setIsRecyclable() should always be paired (one
+ * call to setIsRecyclabe(false) should always be matched with a later call to
+ * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally
+ * reference-counted.
+ *
+ * @param recyclable Whether this item is available to be recycled. Default value
+ * is true.
+ *
+ * @see #isRecyclable()
+ */
+ public final void setIsRecyclable(boolean recyclable) {
+ mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1;
+ if (mIsRecyclableCount < 0) {
+ mIsRecyclableCount = 0;
+ if (DEBUG) {
+ throw new RuntimeException("isRecyclable decremented below 0: "
+ + "unmatched pair of setIsRecyable() calls for " + this);
+ }
+ Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: "
+ + "unmatched pair of setIsRecyable() calls for " + this);
+ } else if (!recyclable && mIsRecyclableCount == 1) {
+ mFlags |= FLAG_NOT_RECYCLABLE;
+ } else if (recyclable && mIsRecyclableCount == 0) {
+ mFlags &= ~FLAG_NOT_RECYCLABLE;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this);
+ }
+ }
+
+ /**
+ * @return true if this item is available to be recycled, false otherwise.
+ *
+ * @see #setIsRecyclable(boolean)
+ */
+ public final boolean isRecyclable() {
+ return (mFlags & FLAG_NOT_RECYCLABLE) == 0
+ && !itemView.hasTransientState();
+ }
+
+ /**
+ * Returns whether we have animations referring to this view holder or not.
+ * This is similar to isRecyclable flag but does not check transient state.
+ */
+ private boolean shouldBeKeptAsChild() {
+ return (mFlags & FLAG_NOT_RECYCLABLE) != 0;
+ }
+
+ /**
+ * @return True if ViewHolder is not referenced by RecyclerView animations but has
+ * transient state which will prevent it from being recycled.
+ */
+ private boolean doesTransientStatePreventRecycling() {
+ return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && itemView.hasTransientState();
+ }
+
+ boolean isUpdated() {
+ return (mFlags & FLAG_UPDATE) != 0;
+ }
+ }
+
+ /**
+ * This method is here so that we can control the important for a11y changes and test it.
+ */
+ @VisibleForTesting
+ boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder,
+ int importantForAccessibility) {
+ if (isComputingLayout()) {
+ viewHolder.mPendingAccessibilityState = importantForAccessibility;
+ mPendingAccessibilityImportanceChange.add(viewHolder);
+ return false;
+ }
+ viewHolder.itemView.setImportantForAccessibility(importantForAccessibility);
+ return true;
+ }
+
+ void dispatchPendingImportantForAccessibilityChanges() {
+ for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) {
+ ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i);
+ if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) {
+ continue;
+ }
+ int state = viewHolder.mPendingAccessibilityState;
+ if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) {
+ //noinspection WrongConstant
+ viewHolder.itemView.setImportantForAccessibility(state);
+ viewHolder.mPendingAccessibilityState =
+ ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET;
+ }
+ }
+ mPendingAccessibilityImportanceChange.clear();
+ }
+
+ int getAdapterPositionFor(ViewHolder viewHolder) {
+ if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
+ | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)
+ || !viewHolder.isBound()) {
+ return RecyclerView.NO_POSITION;
+ }
+ return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition);
+ }
+
+ /**
+ * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of
+ * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged
+ * to create their own subclass of this <code>LayoutParams</code> class
+ * to store any additional required per-child view metadata about the layout.
+ */
+ public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+ ViewHolder mViewHolder;
+ final Rect mDecorInsets = new Rect();
+ boolean mInsetsDirty = true;
+ // Flag is set to true if the view is bound while it is detached from RV.
+ // In this case, we need to manually call invalidate after view is added to guarantee that
+ // invalidation is populated through the View hierarchy
+ boolean mPendingInvalidate = false;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super((ViewGroup.LayoutParams) source);
+ }
+
+ /**
+ * Returns true if the view this LayoutParams is attached to needs to have its content
+ * updated from the corresponding adapter.
+ *
+ * @return true if the view should have its content updated
+ */
+ public boolean viewNeedsUpdate() {
+ return mViewHolder.needsUpdate();
+ }
+
+ /**
+ * Returns true if the view this LayoutParams is attached to is now representing
+ * potentially invalid data. A LayoutManager should scrap/recycle it.
+ *
+ * @return true if the view is invalid
+ */
+ public boolean isViewInvalid() {
+ return mViewHolder.isInvalid();
+ }
+
+ /**
+ * Returns true if the adapter data item corresponding to the view this LayoutParams
+ * is attached to has been removed from the data set. A LayoutManager may choose to
+ * treat it differently in order to animate its outgoing or disappearing state.
+ *
+ * @return true if the item the view corresponds to was removed from the data set
+ */
+ public boolean isItemRemoved() {
+ return mViewHolder.isRemoved();
+ }
+
+ /**
+ * Returns true if the adapter data item corresponding to the view this LayoutParams
+ * is attached to has been changed in the data set. A LayoutManager may choose to
+ * treat it differently in order to animate its changing state.
+ *
+ * @return true if the item the view corresponds to was changed in the data set
+ */
+ public boolean isItemChanged() {
+ return mViewHolder.isUpdated();
+ }
+
+ /**
+ * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()}
+ */
+ @Deprecated
+ public int getViewPosition() {
+ return mViewHolder.getPosition();
+ }
+
+ /**
+ * Returns the adapter position that the view this LayoutParams is attached to corresponds
+ * to as of latest layout calculation.
+ *
+ * @return the adapter position this view as of latest layout pass
+ */
+ public int getViewLayoutPosition() {
+ return mViewHolder.getLayoutPosition();
+ }
+
+ /**
+ * Returns the up-to-date adapter position that the view this LayoutParams is attached to
+ * corresponds to.
+ *
+ * @return the up-to-date adapter position this view. It may return
+ * {@link RecyclerView#NO_POSITION} if item represented by this View has been removed or
+ * its up-to-date position cannot be calculated.
+ */
+ public int getViewAdapterPosition() {
+ return mViewHolder.getAdapterPosition();
+ }
+ }
+
+ /**
+ * Observer base class for watching changes to an {@link Adapter}.
+ * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}.
+ */
+ public abstract static class AdapterDataObserver {
+ public void onChanged() {
+ // Do nothing
+ }
+
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ // do nothing
+ }
+
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ // fallback to onItemRangeChanged(positionStart, itemCount) if app
+ // does not override this method.
+ onItemRangeChanged(positionStart, itemCount);
+ }
+
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ // do nothing
+ }
+
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ // do nothing
+ }
+
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ // do nothing
+ }
+ }
+
+ /**
+ * <p>Base class for smooth scrolling. Handles basic tracking of the target view position and
+ * provides methods to trigger a programmatic scroll.</p>
+ *
+ * @see LinearSmoothScroller
+ */
+ public abstract static class SmoothScroller {
+
+ private int mTargetPosition = RecyclerView.NO_POSITION;
+
+ private RecyclerView mRecyclerView;
+
+ private LayoutManager mLayoutManager;
+
+ private boolean mPendingInitialRun;
+
+ private boolean mRunning;
+
+ private View mTargetView;
+
+ private final Action mRecyclingAction;
+
+ public SmoothScroller() {
+ mRecyclingAction = new Action(0, 0);
+ }
+
+ /**
+ * Starts a smooth scroll for the given target position.
+ * <p>In each animation step, {@link RecyclerView} will check
+ * for the target view and call either
+ * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
+ * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until
+ * SmoothScroller is stopped.</p>
+ *
+ * <p>Note that if RecyclerView finds the target view, it will automatically stop the
+ * SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will
+ * stop calling SmoothScroller in each animation step.</p>
+ */
+ void start(RecyclerView recyclerView, LayoutManager layoutManager) {
+ mRecyclerView = recyclerView;
+ mLayoutManager = layoutManager;
+ if (mTargetPosition == RecyclerView.NO_POSITION) {
+ throw new IllegalArgumentException("Invalid target position");
+ }
+ mRecyclerView.mState.mTargetPosition = mTargetPosition;
+ mRunning = true;
+ mPendingInitialRun = true;
+ mTargetView = findViewByPosition(getTargetPosition());
+ onStart();
+ mRecyclerView.mViewFlinger.postOnAnimation();
+ }
+
+ public void setTargetPosition(int targetPosition) {
+ mTargetPosition = targetPosition;
+ }
+
+ /**
+ * @return The LayoutManager to which this SmoothScroller is attached. Will return
+ * <code>null</code> after the SmoothScroller is stopped.
+ */
+ @Nullable
+ public LayoutManager getLayoutManager() {
+ return mLayoutManager;
+ }
+
+ /**
+ * Stops running the SmoothScroller in each animation callback. Note that this does not
+ * cancel any existing {@link Action} updated by
+ * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
+ * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}.
+ */
+ protected final void stop() {
+ if (!mRunning) {
+ return;
+ }
+ onStop();
+ mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION;
+ mTargetView = null;
+ mTargetPosition = RecyclerView.NO_POSITION;
+ mPendingInitialRun = false;
+ mRunning = false;
+ // trigger a cleanup
+ mLayoutManager.onSmoothScrollerStopped(this);
+ // clear references to avoid any potential leak by a custom smooth scroller
+ mLayoutManager = null;
+ mRecyclerView = null;
+ }
+
+ /**
+ * Returns true if SmoothScroller has been started but has not received the first
+ * animation
+ * callback yet.
+ *
+ * @return True if this SmoothScroller is waiting to start
+ */
+ public boolean isPendingInitialRun() {
+ return mPendingInitialRun;
+ }
+
+
+ /**
+ * @return True if SmoothScroller is currently active
+ */
+ public boolean isRunning() {
+ return mRunning;
+ }
+
+ /**
+ * Returns the adapter position of the target item
+ *
+ * @return Adapter position of the target item or
+ * {@link RecyclerView#NO_POSITION} if no target view is set.
+ */
+ public int getTargetPosition() {
+ return mTargetPosition;
+ }
+
+ private void onAnimation(int dx, int dy) {
+ final RecyclerView recyclerView = mRecyclerView;
+ if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
+ stop();
+ }
+ mPendingInitialRun = false;
+ if (mTargetView != null) {
+ // verify target position
+ if (getChildPosition(mTargetView) == mTargetPosition) {
+ onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
+ mRecyclingAction.runIfNecessary(recyclerView);
+ stop();
+ } else {
+ Log.e(TAG, "Passed over target position while smooth scrolling.");
+ mTargetView = null;
+ }
+ }
+ if (mRunning) {
+ onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
+ boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
+ mRecyclingAction.runIfNecessary(recyclerView);
+ if (hadJumpTarget) {
+ // It is not stopped so needs to be restarted
+ if (mRunning) {
+ mPendingInitialRun = true;
+ recyclerView.mViewFlinger.postOnAnimation();
+ } else {
+ stop(); // done
+ }
+ }
+ }
+ }
+
+ /**
+ * @see RecyclerView#getChildLayoutPosition(android.view.View)
+ */
+ public int getChildPosition(View view) {
+ return mRecyclerView.getChildLayoutPosition(view);
+ }
+
+ /**
+ * @see RecyclerView.LayoutManager#getChildCount()
+ */
+ public int getChildCount() {
+ return mRecyclerView.mLayout.getChildCount();
+ }
+
+ /**
+ * @see RecyclerView.LayoutManager#findViewByPosition(int)
+ */
+ public View findViewByPosition(int position) {
+ return mRecyclerView.mLayout.findViewByPosition(position);
+ }
+
+ /**
+ * @see RecyclerView#scrollToPosition(int)
+ * @deprecated Use {@link Action#jumpTo(int)}.
+ */
+ @Deprecated
+ public void instantScrollToPosition(int position) {
+ mRecyclerView.scrollToPosition(position);
+ }
+
+ protected void onChildAttachedToWindow(View child) {
+ if (getChildPosition(child) == getTargetPosition()) {
+ mTargetView = child;
+ if (DEBUG) {
+ Log.d(TAG, "smooth scroll target view has been attached");
+ }
+ }
+ }
+
+ /**
+ * Normalizes the vector.
+ * @param scrollVector The vector that points to the target scroll position
+ */
+ protected void normalize(PointF scrollVector) {
+ final double magnitude = Math.sqrt(scrollVector.x * scrollVector.x + scrollVector.y
+ * scrollVector.y);
+ scrollVector.x /= magnitude;
+ scrollVector.y /= magnitude;
+ }
+
+ /**
+ * Called when smooth scroll is started. This might be a good time to do setup.
+ */
+ protected abstract void onStart();
+
+ /**
+ * Called when smooth scroller is stopped. This is a good place to cleanup your state etc.
+ * @see #stop()
+ */
+ protected abstract void onStop();
+
+ /**
+ * <p>RecyclerView will call this method each time it scrolls until it can find the target
+ * position in the layout.</p>
+ * <p>SmoothScroller should check dx, dy and if scroll should be changed, update the
+ * provided {@link Action} to define the next scroll.</p>
+ *
+ * @param dx Last scroll amount horizontally
+ * @param dy Last scroll amount vertically
+ * @param state Transient state of RecyclerView
+ * @param action If you want to trigger a new smooth scroll and cancel the previous one,
+ * update this object.
+ */
+ protected abstract void onSeekTargetStep(int dx, int dy, State state, Action action);
+
+ /**
+ * Called when the target position is laid out. This is the last callback SmoothScroller
+ * will receive and it should update the provided {@link Action} to define the scroll
+ * details towards the target view.
+ * @param targetView The view element which render the target position.
+ * @param state Transient state of RecyclerView
+ * @param action Action instance that you should update to define final scroll action
+ * towards the targetView
+ */
+ protected abstract void onTargetFound(View targetView, State state, Action action);
+
+ /**
+ * Holds information about a smooth scroll request by a {@link SmoothScroller}.
+ */
+ public static class Action {
+
+ public static final int UNDEFINED_DURATION = Integer.MIN_VALUE;
+
+ private int mDx;
+
+ private int mDy;
+
+ private int mDuration;
+
+ private int mJumpToPosition = NO_POSITION;
+
+ private Interpolator mInterpolator;
+
+ private boolean mChanged = false;
+
+ // we track this variable to inform custom implementer if they are updating the action
+ // in every animation callback
+ private int mConsecutiveUpdates = 0;
+
+ /**
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ */
+ public Action(int dx, int dy) {
+ this(dx, dy, UNDEFINED_DURATION, null);
+ }
+
+ /**
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ * @param duration Duration of the animation in milliseconds
+ */
+ public Action(int dx, int dy, int duration) {
+ this(dx, dy, duration, null);
+ }
+
+ /**
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ * @param duration Duration of the animation in milliseconds
+ * @param interpolator Interpolator to be used when calculating scroll position in each
+ * animation step
+ */
+ public Action(int dx, int dy, int duration, Interpolator interpolator) {
+ mDx = dx;
+ mDy = dy;
+ mDuration = duration;
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Instead of specifying pixels to scroll, use the target position to jump using
+ * {@link RecyclerView#scrollToPosition(int)}.
+ * <p>
+ * You may prefer using this method if scroll target is really far away and you prefer
+ * to jump to a location and smooth scroll afterwards.
+ * <p>
+ * Note that calling this method takes priority over other update methods such as
+ * {@link #update(int, int, int, Interpolator)}, {@link #setX(float)},
+ * {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call
+ * {@link #jumpTo(int)}, the other changes will not be considered for this animation
+ * frame.
+ *
+ * @param targetPosition The target item position to scroll to using instant scrolling.
+ */
+ public void jumpTo(int targetPosition) {
+ mJumpToPosition = targetPosition;
+ }
+
+ boolean hasJumpTarget() {
+ return mJumpToPosition >= 0;
+ }
+
+ void runIfNecessary(RecyclerView recyclerView) {
+ if (mJumpToPosition >= 0) {
+ final int position = mJumpToPosition;
+ mJumpToPosition = NO_POSITION;
+ recyclerView.jumpToPositionForSmoothScroller(position);
+ mChanged = false;
+ return;
+ }
+ if (mChanged) {
+ validate();
+ if (mInterpolator == null) {
+ if (mDuration == UNDEFINED_DURATION) {
+ recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
+ } else {
+ recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
+ }
+ } else {
+ recyclerView.mViewFlinger.smoothScrollBy(
+ mDx, mDy, mDuration, mInterpolator);
+ }
+ mConsecutiveUpdates++;
+ if (mConsecutiveUpdates > 10) {
+ // A new action is being set in every animation step. This looks like a bad
+ // implementation. Inform developer.
+ Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure"
+ + " you are not changing it unless necessary");
+ }
+ mChanged = false;
+ } else {
+ mConsecutiveUpdates = 0;
+ }
+ }
+
+ private void validate() {
+ if (mInterpolator != null && mDuration < 1) {
+ throw new IllegalStateException("If you provide an interpolator, you must"
+ + " set a positive duration");
+ } else if (mDuration < 1) {
+ throw new IllegalStateException("Scroll duration must be a positive number");
+ }
+ }
+
+ public int getDx() {
+ return mDx;
+ }
+
+ public void setDx(int dx) {
+ mChanged = true;
+ mDx = dx;
+ }
+
+ public int getDy() {
+ return mDy;
+ }
+
+ public void setDy(int dy) {
+ mChanged = true;
+ mDy = dy;
+ }
+
+ public int getDuration() {
+ return mDuration;
+ }
+
+ public void setDuration(int duration) {
+ mChanged = true;
+ mDuration = duration;
+ }
+
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Sets the interpolator to calculate scroll steps
+ * @param interpolator The interpolator to use. If you specify an interpolator, you must
+ * also set the duration.
+ * @see #setDuration(int)
+ */
+ public void setInterpolator(Interpolator interpolator) {
+ mChanged = true;
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Updates the action with given parameters.
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ * @param duration Duration of the animation in milliseconds
+ * @param interpolator Interpolator to be used when calculating scroll position in each
+ * animation step
+ */
+ public void update(int dx, int dy, int duration, Interpolator interpolator) {
+ mDx = dx;
+ mDy = dy;
+ mDuration = duration;
+ mInterpolator = interpolator;
+ mChanged = true;
+ }
+ }
+
+ /**
+ * An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager}
+ * to provide a hint to a {@link SmoothScroller} about the location of the target position.
+ */
+ public interface ScrollVectorProvider {
+ /**
+ * Should calculate the vector that points to the direction where the target position
+ * can be found.
+ * <p>
+ * This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards
+ * the target position.
+ * <p>
+ * The magnitude of the vector is not important. It is always normalized before being
+ * used by the {@link LinearSmoothScroller}.
+ * <p>
+ * LayoutManager should not check whether the position exists in the adapter or not.
+ *
+ * @param targetPosition the target position to which the returned vector should point
+ *
+ * @return the scroll vector for a given position.
+ */
+ PointF computeScrollVectorForPosition(int targetPosition);
+ }
+ }
+
+ static class AdapterDataObservable extends Observable<AdapterDataObserver> {
+ public boolean hasObservers() {
+ return !mObservers.isEmpty();
+ }
+
+ public void notifyChanged() {
+ // since onChanged() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onChanged();
+ }
+ }
+
+ public void notifyItemRangeChanged(int positionStart, int itemCount) {
+ notifyItemRangeChanged(positionStart, itemCount, null);
+ }
+
+ public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ // since onItemRangeChanged() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
+ }
+ }
+
+ public void notifyItemRangeInserted(int positionStart, int itemCount) {
+ // since onItemRangeInserted() is implemented by the app, it could do anything,
+ // including removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ // since onItemRangeRemoved() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemMoved(int fromPosition, int toPosition) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1);
+ }
+ }
+ }
+
+ /**
+ * This is public so that the CREATOR can be access on cold launch.
+ * @hide
+ */
+ public static class SavedState extends AbsSavedState {
+
+ Parcelable mLayoutState;
+
+ /**
+ * called by CREATOR
+ */
+ SavedState(Parcel in) {
+ super(in);
+ mLayoutState = in.readParcelable(LayoutManager.class.getClassLoader());
+ }
+
+ /**
+ * Called by onSaveInstanceState
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeParcelable(mLayoutState, 0);
+ }
+
+ void copyFrom(SavedState other) {
+ mLayoutState = other.mLayoutState;
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+ /**
+ * <p>Contains useful information about the current RecyclerView state like target scroll
+ * position or view focus. State object can also keep arbitrary data, identified by resource
+ * ids.</p>
+ * <p>Often times, RecyclerView components will need to pass information between each other.
+ * To provide a well defined data bus between components, RecyclerView passes the same State
+ * object to component callbacks and these components can use it to exchange data.</p>
+ * <p>If you implement custom components, you can use State's put/get/remove methods to pass
+ * data between your components without needing to manage their lifecycles.</p>
+ */
+ public static class State {
+ static final int STEP_START = 1;
+ static final int STEP_LAYOUT = 1 << 1;
+ static final int STEP_ANIMATIONS = 1 << 2;
+
+ void assertLayoutStep(int accepted) {
+ if ((accepted & mLayoutStep) == 0) {
+ throw new IllegalStateException("Layout state should be one of "
+ + Integer.toBinaryString(accepted) + " but it is "
+ + Integer.toBinaryString(mLayoutStep));
+ }
+ }
+
+
+ /** Owned by SmoothScroller */
+ private int mTargetPosition = RecyclerView.NO_POSITION;
+
+ private SparseArray<Object> mData;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // Fields below are carried from one layout pass to the next
+ ////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Number of items adapter had in the previous layout.
+ */
+ int mPreviousLayoutItemCount = 0;
+
+ /**
+ * Number of items that were NOT laid out but has been deleted from the adapter after the
+ * previous layout.
+ */
+ int mDeletedInvisibleItemCountSincePreviousLayout = 0;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // Fields below must be updated or cleared before they are used (generally before a pass)
+ ////////////////////////////////////////////////////////////////////////////////////////////
+
+ @IntDef(flag = true, value = {
+ STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface LayoutState {}
+
+ @LayoutState
+ int mLayoutStep = STEP_START;
+
+ /**
+ * Number of items adapter has.
+ */
+ int mItemCount = 0;
+
+ boolean mStructureChanged = false;
+
+ boolean mInPreLayout = false;
+
+ boolean mTrackOldChangeHolders = false;
+
+ boolean mIsMeasuring = false;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // Fields below are always reset outside of the pass (or passes) that use them
+ ////////////////////////////////////////////////////////////////////////////////////////////
+
+ boolean mRunSimpleAnimations = false;
+
+ boolean mRunPredictiveAnimations = false;
+
+ /**
+ * This data is saved before a layout calculation happens. After the layout is finished,
+ * if the previously focused view has been replaced with another view for the same item, we
+ * move the focus to the new item automatically.
+ */
+ int mFocusedItemPosition;
+ long mFocusedItemId;
+ // when a sub child has focus, record its id and see if we can directly request focus on
+ // that one instead
+ int mFocusedSubChildId;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////
+
+ State reset() {
+ mTargetPosition = RecyclerView.NO_POSITION;
+ if (mData != null) {
+ mData.clear();
+ }
+ mItemCount = 0;
+ mStructureChanged = false;
+ mIsMeasuring = false;
+ return this;
+ }
+
+ /**
+ * Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially
+ * prior to any layout passes.
+ *
+ * <p>Don't touch any state stored between layout passes, only reset per-layout state, so
+ * that Recycler#getViewForPosition() can function safely.</p>
+ */
+ void prepareForNestedPrefetch(Adapter adapter) {
+ mLayoutStep = STEP_START;
+ mItemCount = adapter.getItemCount();
+ mStructureChanged = false;
+ mInPreLayout = false;
+ mTrackOldChangeHolders = false;
+ mIsMeasuring = false;
+ }
+
+ /**
+ * Returns true if the RecyclerView is currently measuring the layout. This value is
+ * {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView
+ * has non-exact measurement specs.
+ * <p>
+ * Note that if the LayoutManager supports predictive animations and it is calculating the
+ * pre-layout step, this value will be {@code false} even if the RecyclerView is in
+ * {@code onMeasure} call. This is because pre-layout means the previous state of the
+ * RecyclerView and measurements made for that state cannot change the RecyclerView's size.
+ * LayoutManager is always guaranteed to receive another call to
+ * {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens.
+ *
+ * @return True if the RecyclerView is currently calculating its bounds, false otherwise.
+ */
+ public boolean isMeasuring() {
+ return mIsMeasuring;
+ }
+
+ /**
+ * Returns true if
+ * @return
+ */
+ public boolean isPreLayout() {
+ return mInPreLayout;
+ }
+
+ /**
+ * Returns whether RecyclerView will run predictive animations in this layout pass
+ * or not.
+ *
+ * @return true if RecyclerView is calculating predictive animations to be run at the end
+ * of the layout pass.
+ */
+ public boolean willRunPredictiveAnimations() {
+ return mRunPredictiveAnimations;
+ }
+
+ /**
+ * Returns whether RecyclerView will run simple animations in this layout pass
+ * or not.
+ *
+ * @return true if RecyclerView is calculating simple animations to be run at the end of
+ * the layout pass.
+ */
+ public boolean willRunSimpleAnimations() {
+ return mRunSimpleAnimations;
+ }
+
+ /**
+ * Removes the mapping from the specified id, if there was any.
+ * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to
+ * preserve cross functionality and avoid conflicts.
+ */
+ public void remove(int resourceId) {
+ if (mData == null) {
+ return;
+ }
+ mData.remove(resourceId);
+ }
+
+ /**
+ * Gets the Object mapped from the specified id, or <code>null</code>
+ * if no such data exists.
+ *
+ * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.*
+ * to
+ * preserve cross functionality and avoid conflicts.
+ */
+ public <T> T get(int resourceId) {
+ if (mData == null) {
+ return null;
+ }
+ return (T) mData.get(resourceId);
+ }
+
+ /**
+ * Adds a mapping from the specified id to the specified value, replacing the previous
+ * mapping from the specified key if there was one.
+ *
+ * @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to
+ * preserve cross functionality and avoid conflicts.
+ * @param data The data you want to associate with the resourceId.
+ */
+ public void put(int resourceId, Object data) {
+ if (mData == null) {
+ mData = new SparseArray<Object>();
+ }
+ mData.put(resourceId, data);
+ }
+
+ /**
+ * If scroll is triggered to make a certain item visible, this value will return the
+ * adapter index of that item.
+ * @return Adapter index of the target item or
+ * {@link RecyclerView#NO_POSITION} if there is no target
+ * position.
+ */
+ public int getTargetScrollPosition() {
+ return mTargetPosition;
+ }
+
+ /**
+ * Returns if current scroll has a target position.
+ * @return true if scroll is being triggered to make a certain position visible
+ * @see #getTargetScrollPosition()
+ */
+ public boolean hasTargetScrollPosition() {
+ return mTargetPosition != RecyclerView.NO_POSITION;
+ }
+
+ /**
+ * @return true if the structure of the data set has changed since the last call to
+ * onLayoutChildren, false otherwise
+ */
+ public boolean didStructureChange() {
+ return mStructureChanged;
+ }
+
+ /**
+ * Returns the total number of items that can be laid out. Note that this number is not
+ * necessarily equal to the number of items in the adapter, so you should always use this
+ * number for your position calculations and never access the adapter directly.
+ * <p>
+ * RecyclerView listens for Adapter's notify events and calculates the effects of adapter
+ * data changes on existing Views. These calculations are used to decide which animations
+ * should be run.
+ * <p>
+ * To support predictive animations, RecyclerView may rewrite or reorder Adapter changes to
+ * present the correct state to LayoutManager in pre-layout pass.
+ * <p>
+ * For example, a newly added item is not included in pre-layout item count because
+ * pre-layout reflects the contents of the adapter before the item is added. Behind the
+ * scenes, RecyclerView offsets {@link Recycler#getViewForPosition(int)} calls such that
+ * LayoutManager does not know about the new item's existence in pre-layout. The item will
+ * be available in second layout pass and will be included in the item count. Similar
+ * adjustments are made for moved and removed items as well.
+ * <p>
+ * You can get the adapter's item count via {@link LayoutManager#getItemCount()} method.
+ *
+ * @return The number of items currently available
+ * @see LayoutManager#getItemCount()
+ */
+ public int getItemCount() {
+ return mInPreLayout
+ ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout)
+ : mItemCount;
+ }
+
+ @Override
+ public String toString() {
+ return "State{"
+ + "mTargetPosition=" + mTargetPosition
+ + ", mData=" + mData
+ + ", mItemCount=" + mItemCount
+ + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount
+ + ", mDeletedInvisibleItemCountSincePreviousLayout="
+ + mDeletedInvisibleItemCountSincePreviousLayout
+ + ", mStructureChanged=" + mStructureChanged
+ + ", mInPreLayout=" + mInPreLayout
+ + ", mRunSimpleAnimations=" + mRunSimpleAnimations
+ + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations
+ + '}';
+ }
+ }
+
+ /**
+ * This class defines the behavior of fling if the developer wishes to handle it.
+ * <p>
+ * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior.
+ *
+ * @see #setOnFlingListener(OnFlingListener)
+ */
+ public abstract static class OnFlingListener {
+
+ /**
+ * Override this to handle a fling given the velocities in both x and y directions.
+ * Note that this method will only be called if the associated {@link LayoutManager}
+ * supports scrolling and the fling is not handled by nested scrolls first.
+ *
+ * @param velocityX the fling velocity on the X axis
+ * @param velocityY the fling velocity on the Y axis
+ *
+ * @return true if the fling washandled, false otherwise.
+ */
+ public abstract boolean onFling(int velocityX, int velocityY);
+ }
+
+ /**
+ * Internal listener that manages items after animations finish. This is how items are
+ * retained (not recycled) during animations, but allowed to be recycled afterwards.
+ * It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished()
+ * method on the animator's listener when it is done animating any item.
+ */
+ private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {
+
+ ItemAnimatorRestoreListener() {
+ }
+
+ @Override
+ public void onAnimationFinished(ViewHolder item) {
+ item.setIsRecyclable(true);
+ if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh
+ item.mShadowedHolder = null;
+ }
+ // always null this because an OldViewHolder can never become NewViewHolder w/o being
+ // recycled.
+ item.mShadowingHolder = null;
+ if (!item.shouldBeKeptAsChild()) {
+ if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {
+ removeDetachedView(item.itemView, false);
+ }
+ }
+ }
+ }
+
+ /**
+ * This class defines the animations that take place on items as changes are made
+ * to the adapter.
+ *
+ * Subclasses of ItemAnimator can be used to implement custom animations for actions on
+ * ViewHolder items. The RecyclerView will manage retaining these items while they
+ * are being animated, but implementors must call {@link #dispatchAnimationFinished(ViewHolder)}
+ * when a ViewHolder's animation is finished. In other words, there must be a matching
+ * {@link #dispatchAnimationFinished(ViewHolder)} call for each
+ * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) animateAppearance()},
+ * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateChange()}
+ * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) animatePersistence()},
+ * and
+ * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateDisappearance()} call.
+ *
+ * <p>By default, RecyclerView uses {@link DefaultItemAnimator}.</p>
+ *
+ * @see #setItemAnimator(ItemAnimator)
+ */
+ @SuppressWarnings("UnusedParameters")
+ public abstract static class ItemAnimator {
+
+ /**
+ * The Item represented by this ViewHolder is updated.
+ * <p>
+ * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+ */
+ public static final int FLAG_CHANGED = ViewHolder.FLAG_UPDATE;
+
+ /**
+ * The Item represented by this ViewHolder is removed from the adapter.
+ * <p>
+ * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+ */
+ public static final int FLAG_REMOVED = ViewHolder.FLAG_REMOVED;
+
+ /**
+ * Adapter {@link Adapter#notifyDataSetChanged()} has been called and the content
+ * represented by this ViewHolder is invalid.
+ * <p>
+ * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+ */
+ public static final int FLAG_INVALIDATED = ViewHolder.FLAG_INVALID;
+
+ /**
+ * The position of the Item represented by this ViewHolder has been changed. This flag is
+ * not bound to {@link Adapter#notifyItemMoved(int, int)}. It might be set in response to
+ * any adapter change that may have a side effect on this item. (e.g. The item before this
+ * one has been removed from the Adapter).
+ * <p>
+ * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+ */
+ public static final int FLAG_MOVED = ViewHolder.FLAG_MOVED;
+
+ /**
+ * This ViewHolder was not laid out but has been added to the layout in pre-layout state
+ * by the {@link LayoutManager}. This means that the item was already in the Adapter but
+ * invisible and it may become visible in the post layout phase. LayoutManagers may prefer
+ * to add new items in pre-layout to specify their virtual location when they are invisible
+ * (e.g. to specify the item should <i>animate in</i> from below the visible area).
+ * <p>
+ * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+ */
+ public static final int FLAG_APPEARED_IN_PRE_LAYOUT =
+ ViewHolder.FLAG_APPEARED_IN_PRE_LAYOUT;
+
+ /**
+ * The set of flags that might be passed to
+ * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ */
+ @IntDef(flag = true, value = {
+ FLAG_CHANGED, FLAG_REMOVED, FLAG_MOVED, FLAG_INVALIDATED,
+ FLAG_APPEARED_IN_PRE_LAYOUT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AdapterChanges {}
+ private ItemAnimatorListener mListener = null;
+ private ArrayList<ItemAnimatorFinishedListener> mFinishedListeners =
+ new ArrayList<ItemAnimatorFinishedListener>();
+
+ private long mAddDuration = 120;
+ private long mRemoveDuration = 120;
+ private long mMoveDuration = 250;
+ private long mChangeDuration = 250;
+
+ /**
+ * Gets the current duration for which all move animations will run.
+ *
+ * @return The current move duration
+ */
+ public long getMoveDuration() {
+ return mMoveDuration;
+ }
+
+ /**
+ * Sets the duration for which all move animations will run.
+ *
+ * @param moveDuration The move duration
+ */
+ public void setMoveDuration(long moveDuration) {
+ mMoveDuration = moveDuration;
+ }
+
+ /**
+ * Gets the current duration for which all add animations will run.
+ *
+ * @return The current add duration
+ */
+ public long getAddDuration() {
+ return mAddDuration;
+ }
+
+ /**
+ * Sets the duration for which all add animations will run.
+ *
+ * @param addDuration The add duration
+ */
+ public void setAddDuration(long addDuration) {
+ mAddDuration = addDuration;
+ }
+
+ /**
+ * Gets the current duration for which all remove animations will run.
+ *
+ * @return The current remove duration
+ */
+ public long getRemoveDuration() {
+ return mRemoveDuration;
+ }
+
+ /**
+ * Sets the duration for which all remove animations will run.
+ *
+ * @param removeDuration The remove duration
+ */
+ public void setRemoveDuration(long removeDuration) {
+ mRemoveDuration = removeDuration;
+ }
+
+ /**
+ * Gets the current duration for which all change animations will run.
+ *
+ * @return The current change duration
+ */
+ public long getChangeDuration() {
+ return mChangeDuration;
+ }
+
+ /**
+ * Sets the duration for which all change animations will run.
+ *
+ * @param changeDuration The change duration
+ */
+ public void setChangeDuration(long changeDuration) {
+ mChangeDuration = changeDuration;
+ }
+
+ /**
+ * Internal only:
+ * Sets the listener that must be called when the animator is finished
+ * animating the item (or immediately if no animation happens). This is set
+ * internally and is not intended to be set by external code.
+ *
+ * @param listener The listener that must be called.
+ */
+ void setListener(ItemAnimatorListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Called by the RecyclerView before the layout begins. Item animator should record
+ * necessary information about the View before it is potentially rebound, moved or removed.
+ * <p>
+ * The data returned from this method will be passed to the related <code>animate**</code>
+ * methods.
+ * <p>
+ * Note that this method may be called after pre-layout phase if LayoutManager adds new
+ * Views to the layout in pre-layout pass.
+ * <p>
+ * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of
+ * the View and the adapter change flags.
+ *
+ * @param state The current State of RecyclerView which includes some useful data
+ * about the layout that will be calculated.
+ * @param viewHolder The ViewHolder whose information should be recorded.
+ * @param changeFlags Additional information about what changes happened in the Adapter
+ * about the Item represented by this ViewHolder. For instance, if
+ * item is deleted from the adapter, {@link #FLAG_REMOVED} will be set.
+ * @param payloads The payload list that was previously passed to
+ * {@link Adapter#notifyItemChanged(int, Object)} or
+ * {@link Adapter#notifyItemRangeChanged(int, int, Object)}.
+ *
+ * @return An ItemHolderInfo instance that preserves necessary information about the
+ * ViewHolder. This object will be passed back to related <code>animate**</code> methods
+ * after layout is complete.
+ *
+ * @see #recordPostLayoutInformation(State, ViewHolder)
+ * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ */
+ public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
+ @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
+ @NonNull List<Object> payloads) {
+ return obtainHolderInfo().setFrom(viewHolder);
+ }
+
+ /**
+ * Called by the RecyclerView after the layout is complete. Item animator should record
+ * necessary information about the View's final state.
+ * <p>
+ * The data returned from this method will be passed to the related <code>animate**</code>
+ * methods.
+ * <p>
+ * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of
+ * the View.
+ *
+ * @param state The current State of RecyclerView which includes some useful data about
+ * the layout that will be calculated.
+ * @param viewHolder The ViewHolder whose information should be recorded.
+ *
+ * @return An ItemHolderInfo that preserves necessary information about the ViewHolder.
+ * This object will be passed back to related <code>animate**</code> methods when
+ * RecyclerView decides how items should be animated.
+ *
+ * @see #recordPreLayoutInformation(State, ViewHolder, int, List)
+ * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ */
+ public @NonNull ItemHolderInfo recordPostLayoutInformation(@NonNull State state,
+ @NonNull ViewHolder viewHolder) {
+ return obtainHolderInfo().setFrom(viewHolder);
+ }
+
+ /**
+ * Called by the RecyclerView when a ViewHolder has disappeared from the layout.
+ * <p>
+ * This means that the View was a child of the LayoutManager when layout started but has
+ * been removed by the LayoutManager. It might have been removed from the adapter or simply
+ * become invisible due to other factors. You can distinguish these two cases by checking
+ * the change flags that were passed to
+ * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ * <p>
+ * Note that when a ViewHolder both changes and disappears in the same layout pass, the
+ * animation callback method which will be called by the RecyclerView depends on the
+ * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the
+ * LayoutManager's decision whether to layout the changed version of a disappearing
+ * ViewHolder or not. RecyclerView will call
+ * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateChange} instead of {@code animateDisappearance} if and only if the ItemAnimator
+ * returns {@code false} from
+ * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the
+ * LayoutManager lays out a new disappearing view that holds the updated information.
+ * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views.
+ * <p>
+ * If LayoutManager supports predictive animations, it might provide a target disappear
+ * location for the View by laying it out in that location. When that happens,
+ * RecyclerView will call {@link #recordPostLayoutInformation(State, ViewHolder)} and the
+ * response of that call will be passed to this method as the <code>postLayoutInfo</code>.
+ * <p>
+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
+ * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
+ * decides not to animate the view).
+ *
+ * @param viewHolder The ViewHolder which should be animated
+ * @param preLayoutInfo The information that was returned from
+ * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ * @param postLayoutInfo The information that was returned from
+ * {@link #recordPostLayoutInformation(State, ViewHolder)}. Might be
+ * null if the LayoutManager did not layout the item.
+ *
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder,
+ @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo);
+
+ /**
+ * Called by the RecyclerView when a ViewHolder is added to the layout.
+ * <p>
+ * In detail, this means that the ViewHolder was <b>not</b> a child when the layout started
+ * but has been added by the LayoutManager. It might be newly added to the adapter or
+ * simply become visible due to other factors.
+ * <p>
+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
+ * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
+ * decides not to animate the view).
+ *
+ * @param viewHolder The ViewHolder which should be animated
+ * @param preLayoutInfo The information that was returned from
+ * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ * Might be null if Item was just added to the adapter or
+ * LayoutManager does not support predictive animations or it could
+ * not predict that this ViewHolder will become visible.
+ * @param postLayoutInfo The information that was returned from {@link
+ * #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ *
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder,
+ @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
+
+ /**
+ * Called by the RecyclerView when a ViewHolder is present in both before and after the
+ * layout and RecyclerView has not received a {@link Adapter#notifyItemChanged(int)} call
+ * for it or a {@link Adapter#notifyDataSetChanged()} call.
+ * <p>
+ * This ViewHolder still represents the same data that it was representing when the layout
+ * started but its position / size may be changed by the LayoutManager.
+ * <p>
+ * If the Item's layout position didn't change, RecyclerView still calls this method because
+ * it does not track this information (or does not necessarily know that an animation is
+ * not required). Your ItemAnimator should handle this case and if there is nothing to
+ * animate, it should call {@link #dispatchAnimationFinished(ViewHolder)} and return
+ * <code>false</code>.
+ * <p>
+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation
+ * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it
+ * decides not to animate the view).
+ *
+ * @param viewHolder The ViewHolder which should be animated
+ * @param preLayoutInfo The information that was returned from
+ * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ * @param postLayoutInfo The information that was returned from {@link
+ * #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ *
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder,
+ @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
+
+ /**
+ * Called by the RecyclerView when an adapter item is present both before and after the
+ * layout and RecyclerView has received a {@link Adapter#notifyItemChanged(int)} call
+ * for it. This method may also be called when
+ * {@link Adapter#notifyDataSetChanged()} is called and adapter has stable ids so that
+ * RecyclerView could still rebind views to the same ViewHolders. If viewType changes when
+ * {@link Adapter#notifyDataSetChanged()} is called, this method <b>will not</b> be called,
+ * instead, {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} will be
+ * called for the new ViewHolder and the old one will be recycled.
+ * <p>
+ * If this method is called due to a {@link Adapter#notifyDataSetChanged()} call, there is
+ * a good possibility that item contents didn't really change but it is rebound from the
+ * adapter. {@link DefaultItemAnimator} will skip animating the View if its location on the
+ * screen didn't change and your animator should handle this case as well and avoid creating
+ * unnecessary animations.
+ * <p>
+ * When an item is updated, ItemAnimator has a chance to ask RecyclerView to keep the
+ * previous presentation of the item as-is and supply a new ViewHolder for the updated
+ * presentation (see: {@link #canReuseUpdatedViewHolder(ViewHolder, List)}.
+ * This is useful if you don't know the contents of the Item and would like
+ * to cross-fade the old and the new one ({@link DefaultItemAnimator} uses this technique).
+ * <p>
+ * When you are writing a custom item animator for your layout, it might be more performant
+ * and elegant to re-use the same ViewHolder and animate the content changes manually.
+ * <p>
+ * When {@link Adapter#notifyItemChanged(int)} is called, the Item's view type may change.
+ * If the Item's view type has changed or ItemAnimator returned <code>false</code> for
+ * this ViewHolder when {@link #canReuseUpdatedViewHolder(ViewHolder, List)} was called, the
+ * <code>oldHolder</code> and <code>newHolder</code> will be different ViewHolder instances
+ * which represent the same Item. In that case, only the new ViewHolder is visible
+ * to the LayoutManager but RecyclerView keeps old ViewHolder attached for animations.
+ * <p>
+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} for each distinct
+ * ViewHolder when their animation is complete
+ * (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it decides not to
+ * animate the view).
+ * <p>
+ * If oldHolder and newHolder are the same instance, you should call
+ * {@link #dispatchAnimationFinished(ViewHolder)} <b>only once</b>.
+ * <p>
+ * Note that when a ViewHolder both changes and disappears in the same layout pass, the
+ * animation callback method which will be called by the RecyclerView depends on the
+ * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the
+ * LayoutManager's decision whether to layout the changed version of a disappearing
+ * ViewHolder or not. RecyclerView will call
+ * {@code animateChange} instead of
+ * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateDisappearance} if and only if the ItemAnimator returns {@code false} from
+ * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the
+ * LayoutManager lays out a new disappearing view that holds the updated information.
+ * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views.
+ *
+ * @param oldHolder The ViewHolder before the layout is started, might be the same
+ * instance with newHolder.
+ * @param newHolder The ViewHolder after the layout is finished, might be the same
+ * instance with oldHolder.
+ * @param preLayoutInfo The information that was returned from
+ * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ * @param postLayoutInfo The information that was returned from {@link
+ * #recordPreLayoutInformation(State, ViewHolder, int, List)}.
+ *
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animateChange(@NonNull ViewHolder oldHolder,
+ @NonNull ViewHolder newHolder,
+ @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
+
+ @AdapterChanges static int buildAdapterChangeFlagsForAnimations(ViewHolder viewHolder) {
+ int flags = viewHolder.mFlags & (FLAG_INVALIDATED | FLAG_REMOVED | FLAG_CHANGED);
+ if (viewHolder.isInvalid()) {
+ return FLAG_INVALIDATED;
+ }
+ if ((flags & FLAG_INVALIDATED) == 0) {
+ final int oldPos = viewHolder.getOldPosition();
+ final int pos = viewHolder.getAdapterPosition();
+ if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) {
+ flags |= FLAG_MOVED;
+ }
+ }
+ return flags;
+ }
+
+ /**
+ * Called when there are pending animations waiting to be started. This state
+ * is governed by the return values from
+ * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateAppearance()},
+ * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateChange()}
+ * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animatePersistence()}, and
+ * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateDisappearance()}, which inform the RecyclerView that the ItemAnimator wants to be
+ * called later to start the associated animations. runPendingAnimations() will be scheduled
+ * to be run on the next frame.
+ */
+ public abstract void runPendingAnimations();
+
+ /**
+ * Method called when an animation on a view should be ended immediately.
+ * This could happen when other events, like scrolling, occur, so that
+ * animating views can be quickly put into their proper end locations.
+ * Implementations should ensure that any animations running on the item
+ * are canceled and affected properties are set to their end values.
+ * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished
+ * animation since the animations are effectively done when this method is called.
+ *
+ * @param item The item for which an animation should be stopped.
+ */
+ public abstract void endAnimation(ViewHolder item);
+
+ /**
+ * Method called when all item animations should be ended immediately.
+ * This could happen when other events, like scrolling, occur, so that
+ * animating views can be quickly put into their proper end locations.
+ * Implementations should ensure that any animations running on any items
+ * are canceled and affected properties are set to their end values.
+ * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished
+ * animation since the animations are effectively done when this method is called.
+ */
+ public abstract void endAnimations();
+
+ /**
+ * Method which returns whether there are any item animations currently running.
+ * This method can be used to determine whether to delay other actions until
+ * animations end.
+ *
+ * @return true if there are any item animations currently running, false otherwise.
+ */
+ public abstract boolean isRunning();
+
+ /**
+ * Method to be called by subclasses when an animation is finished.
+ * <p>
+ * For each call RecyclerView makes to
+ * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateAppearance()},
+ * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animatePersistence()}, or
+ * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateDisappearance()}, there
+ * should
+ * be a matching {@link #dispatchAnimationFinished(ViewHolder)} call by the subclass.
+ * <p>
+ * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateChange()}, subclass should call this method for both the <code>oldHolder</code>
+ * and <code>newHolder</code> (if they are not the same instance).
+ *
+ * @param viewHolder The ViewHolder whose animation is finished.
+ * @see #onAnimationFinished(ViewHolder)
+ */
+ public final void dispatchAnimationFinished(ViewHolder viewHolder) {
+ onAnimationFinished(viewHolder);
+ if (mListener != null) {
+ mListener.onAnimationFinished(viewHolder);
+ }
+ }
+
+ /**
+ * Called after {@link #dispatchAnimationFinished(ViewHolder)} is called by the
+ * ItemAnimator.
+ *
+ * @param viewHolder The ViewHolder whose animation is finished. There might still be other
+ * animations running on this ViewHolder.
+ * @see #dispatchAnimationFinished(ViewHolder)
+ */
+ public void onAnimationFinished(ViewHolder viewHolder) {
+ }
+
+ /**
+ * Method to be called by subclasses when an animation is started.
+ * <p>
+ * For each call RecyclerView makes to
+ * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateAppearance()},
+ * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animatePersistence()}, or
+ * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateDisappearance()}, there should be a matching
+ * {@link #dispatchAnimationStarted(ViewHolder)} call by the subclass.
+ * <p>
+ * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * animateChange()}, subclass should call this method for both the <code>oldHolder</code>
+ * and <code>newHolder</code> (if they are not the same instance).
+ * <p>
+ * If your ItemAnimator decides not to animate a ViewHolder, it should call
+ * {@link #dispatchAnimationFinished(ViewHolder)} <b>without</b> calling
+ * {@link #dispatchAnimationStarted(ViewHolder)}.
+ *
+ * @param viewHolder The ViewHolder whose animation is starting.
+ * @see #onAnimationStarted(ViewHolder)
+ */
+ public final void dispatchAnimationStarted(ViewHolder viewHolder) {
+ onAnimationStarted(viewHolder);
+ }
+
+ /**
+ * Called when a new animation is started on the given ViewHolder.
+ *
+ * @param viewHolder The ViewHolder which started animating. Note that the ViewHolder
+ * might already be animating and this might be another animation.
+ * @see #dispatchAnimationStarted(ViewHolder)
+ */
+ public void onAnimationStarted(ViewHolder viewHolder) {
+
+ }
+
+ /**
+ * Like {@link #isRunning()}, this method returns whether there are any item
+ * animations currently running. Additionally, the listener passed in will be called
+ * when there are no item animations running, either immediately (before the method
+ * returns) if no animations are currently running, or when the currently running
+ * animations are {@link #dispatchAnimationsFinished() finished}.
+ *
+ * <p>Note that the listener is transient - it is either called immediately and not
+ * stored at all, or stored only until it is called when running animations
+ * are finished sometime later.</p>
+ *
+ * @param listener A listener to be called immediately if no animations are running
+ * or later when currently-running animations have finished. A null listener is
+ * equivalent to calling {@link #isRunning()}.
+ * @return true if there are any item animations currently running, false otherwise.
+ */
+ public final boolean isRunning(ItemAnimatorFinishedListener listener) {
+ boolean running = isRunning();
+ if (listener != null) {
+ if (!running) {
+ listener.onAnimationsFinished();
+ } else {
+ mFinishedListeners.add(listener);
+ }
+ }
+ return running;
+ }
+
+ /**
+ * When an item is changed, ItemAnimator can decide whether it wants to re-use
+ * the same ViewHolder for animations or RecyclerView should create a copy of the
+ * item and ItemAnimator will use both to run the animation (e.g. cross-fade).
+ * <p>
+ * Note that this method will only be called if the {@link ViewHolder} still has the same
+ * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive
+ * both {@link ViewHolder}s in the
+ * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method.
+ * <p>
+ * If your application is using change payloads, you can override
+ * {@link #canReuseUpdatedViewHolder(ViewHolder, List)} to decide based on payloads.
+ *
+ * @param viewHolder The ViewHolder which represents the changed item's old content.
+ *
+ * @return True if RecyclerView should just rebind to the same ViewHolder or false if
+ * RecyclerView should create a new ViewHolder and pass this ViewHolder to the
+ * ItemAnimator to animate. Default implementation returns <code>true</code>.
+ *
+ * @see #canReuseUpdatedViewHolder(ViewHolder, List)
+ */
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) {
+ return true;
+ }
+
+ /**
+ * When an item is changed, ItemAnimator can decide whether it wants to re-use
+ * the same ViewHolder for animations or RecyclerView should create a copy of the
+ * item and ItemAnimator will use both to run the animation (e.g. cross-fade).
+ * <p>
+ * Note that this method will only be called if the {@link ViewHolder} still has the same
+ * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive
+ * both {@link ViewHolder}s in the
+ * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method.
+ *
+ * @param viewHolder The ViewHolder which represents the changed item's old content.
+ * @param payloads A non-null list of merged payloads that were sent with change
+ * notifications. Can be empty if the adapter is invalidated via
+ * {@link RecyclerView.Adapter#notifyDataSetChanged()}. The same list of
+ * payloads will be passed into
+ * {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)}
+ * method <b>if</b> this method returns <code>true</code>.
+ *
+ * @return True if RecyclerView should just rebind to the same ViewHolder or false if
+ * RecyclerView should create a new ViewHolder and pass this ViewHolder to the
+ * ItemAnimator to animate. Default implementation calls
+ * {@link #canReuseUpdatedViewHolder(ViewHolder)}.
+ *
+ * @see #canReuseUpdatedViewHolder(ViewHolder)
+ */
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+ @NonNull List<Object> payloads) {
+ return canReuseUpdatedViewHolder(viewHolder);
+ }
+
+ /**
+ * This method should be called by ItemAnimator implementations to notify
+ * any listeners that all pending and active item animations are finished.
+ */
+ public final void dispatchAnimationsFinished() {
+ final int count = mFinishedListeners.size();
+ for (int i = 0; i < count; ++i) {
+ mFinishedListeners.get(i).onAnimationsFinished();
+ }
+ mFinishedListeners.clear();
+ }
+
+ /**
+ * Returns a new {@link ItemHolderInfo} which will be used to store information about the
+ * ViewHolder. This information will later be passed into <code>animate**</code> methods.
+ * <p>
+ * You can override this method if you want to extend {@link ItemHolderInfo} and provide
+ * your own instances.
+ *
+ * @return A new {@link ItemHolderInfo}.
+ */
+ public ItemHolderInfo obtainHolderInfo() {
+ return new ItemHolderInfo();
+ }
+
+ /**
+ * The interface to be implemented by listeners to animation events from this
+ * ItemAnimator. This is used internally and is not intended for developers to
+ * create directly.
+ */
+ interface ItemAnimatorListener {
+ void onAnimationFinished(ViewHolder item);
+ }
+
+ /**
+ * This interface is used to inform listeners when all pending or running animations
+ * in an ItemAnimator are finished. This can be used, for example, to delay an action
+ * in a data set until currently-running animations are complete.
+ *
+ * @see #isRunning(ItemAnimatorFinishedListener)
+ */
+ public interface ItemAnimatorFinishedListener {
+ /**
+ * Notifies when all pending or running animations in an ItemAnimator are finished.
+ */
+ void onAnimationsFinished();
+ }
+
+ /**
+ * A simple data structure that holds information about an item's bounds.
+ * This information is used in calculating item animations. Default implementation of
+ * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)} and
+ * {@link #recordPostLayoutInformation(RecyclerView.State, ViewHolder)} returns this data
+ * structure. You can extend this class if you would like to keep more information about
+ * the Views.
+ * <p>
+ * If you want to provide your own implementation but still use `super` methods to record
+ * basic information, you can override {@link #obtainHolderInfo()} to provide your own
+ * instances.
+ */
+ public static class ItemHolderInfo {
+
+ /**
+ * The left edge of the View (excluding decorations)
+ */
+ public int left;
+
+ /**
+ * The top edge of the View (excluding decorations)
+ */
+ public int top;
+
+ /**
+ * The right edge of the View (excluding decorations)
+ */
+ public int right;
+
+ /**
+ * The bottom edge of the View (excluding decorations)
+ */
+ public int bottom;
+
+ /**
+ * The change flags that were passed to
+ * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)}.
+ */
+ @AdapterChanges
+ public int changeFlags;
+
+ public ItemHolderInfo() {
+ }
+
+ /**
+ * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from
+ * the given ViewHolder. Clears all {@link #changeFlags}.
+ *
+ * @param holder The ViewHolder whose bounds should be copied.
+ * @return This {@link ItemHolderInfo}
+ */
+ public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder) {
+ return setFrom(holder, 0);
+ }
+
+ /**
+ * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from
+ * the given ViewHolder and sets the {@link #changeFlags} to the given flags parameter.
+ *
+ * @param holder The ViewHolder whose bounds should be copied.
+ * @param flags The adapter change flags that were passed into
+ * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int,
+ * List)}.
+ * @return This {@link ItemHolderInfo}
+ */
+ public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder,
+ @AdapterChanges int flags) {
+ final View view = holder.itemView;
+ this.left = view.getLeft();
+ this.top = view.getTop();
+ this.right = view.getRight();
+ this.bottom = view.getBottom();
+ return this;
+ }
+ }
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ if (mChildDrawingOrderCallback == null) {
+ return super.getChildDrawingOrder(childCount, i);
+ } else {
+ return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i);
+ }
+ }
+
+ /**
+ * A callback interface that can be used to alter the drawing order of RecyclerView children.
+ * <p>
+ * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case
+ * that applies to that method also applies to this callback. For example, changing the drawing
+ * order of two views will not have any effect if their elevation values are different since
+ * elevation overrides the result of this callback.
+ */
+ public interface ChildDrawingOrderCallback {
+ /**
+ * Returns the index of the child to draw for this iteration. Override this
+ * if you want to change the drawing order of children. By default, it
+ * returns i.
+ *
+ * @param i The current iteration.
+ * @return The index of the child to draw this iteration.
+ *
+ * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback)
+ */
+ int onGetChildDrawingOrder(int childCount, int i);
+ }
+}
diff --git a/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java b/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java
new file mode 100644
index 0000000..282da64
--- /dev/null
+++ b/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * The AccessibilityDelegate used by RecyclerView.
+ * <p>
+ * This class handles basic accessibility actions and delegates them to LayoutManager.
+ */
+public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegate {
+ final RecyclerView mRecyclerView;
+
+
+ public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ }
+
+ boolean shouldIgnore() {
+ return mRecyclerView.hasPendingAdapterUpdates();
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (super.performAccessibilityAction(host, action, args)) {
+ return true;
+ }
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+ return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setClassName(RecyclerView.class.getName());
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+ mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ event.setClassName(RecyclerView.class.getName());
+ if (host instanceof RecyclerView && !shouldIgnore()) {
+ RecyclerView rv = (RecyclerView) host;
+ if (rv.getLayoutManager() != null) {
+ rv.getLayoutManager().onInitializeAccessibilityEvent(event);
+ }
+ }
+ }
+
+ /**
+ * Gets the AccessibilityDelegate for an individual item in the RecyclerView.
+ * A basic item delegate is provided by default, but you can override this
+ * method to provide a custom per-item delegate.
+ */
+ public AccessibilityDelegate getItemDelegate() {
+ return mItemDelegate;
+ }
+
+ final AccessibilityDelegate mItemDelegate = new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+ mRecyclerView.getLayoutManager()
+ .onInitializeAccessibilityNodeInfoForItem(host, info);
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (super.performAccessibilityAction(host, action, args)) {
+ return true;
+ }
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
+ return mRecyclerView.getLayoutManager()
+ .performAccessibilityActionForItem(host, action, args);
+ }
+ return false;
+ }
+ };
+}
+
diff --git a/com/android/internal/widget/RemeasuringLinearLayout.java b/com/android/internal/widget/RemeasuringLinearLayout.java
new file mode 100644
index 0000000..7b154a5
--- /dev/null
+++ b/com/android/internal/widget/RemeasuringLinearLayout.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+
+/**
+ * A LinearLayout that sets it's height again after the last measure pass. This is needed for
+ * MessagingLayouts where groups need to be able to snap it's height to.
+ */
[email protected]
+public class RemeasuringLinearLayout extends LinearLayout {
+
+ private ArrayList<View> mMatchParentViews = new ArrayList<>();
+
+ public RemeasuringLinearLayout(Context context) {
+ super(context);
+ }
+
+ public RemeasuringLinearLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public RemeasuringLinearLayout(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public RemeasuringLinearLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int count = getChildCount();
+ int height = 0;
+ boolean isVertical = getOrientation() == LinearLayout.VERTICAL;
+ boolean isWrapContent = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
+ for (int i = 0; i < count; ++i) {
+ final View child = getChildAt(i);
+ if (child == null || child.getVisibility() == View.GONE) {
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!isWrapContent || lp.height != LayoutParams.MATCH_PARENT || isVertical) {
+ int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
+ height = Math.max(height, isVertical ? height + childHeight : childHeight);
+ } else {
+ // We have match parent children in a wrap content view, let's measure the
+ // view properly
+ mMatchParentViews.add(child);
+ }
+ }
+ if (mMatchParentViews.size() > 0) {
+ int exactHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ for (View child : mMatchParentViews) {
+ child.measure(getChildMeasureSpec(
+ widthMeasureSpec, getPaddingStart() + getPaddingEnd(),
+ child.getLayoutParams().width),
+ exactHeightSpec);
+ }
+ }
+ mMatchParentViews.clear();
+ setMeasuredDimension(getMeasuredWidth(), height);
+ }
+}
diff --git a/com/android/internal/widget/ResolverDrawerLayout.java b/com/android/internal/widget/ResolverDrawerLayout.java
new file mode 100644
index 0000000..fb2ecf3
--- /dev/null
+++ b/com/android/internal/widget/ResolverDrawerLayout.java
@@ -0,0 +1,1214 @@
+/*
+ * 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.internal.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.metrics.LogMaker;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+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.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView;
+import android.widget.OverScroller;
+
+import com.android.internal.R;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+public class ResolverDrawerLayout extends ViewGroup {
+ private static final String TAG = "ResolverDrawerLayout";
+ private MetricsLogger mMetricsLogger;
+
+ /**
+ * Max width of the whole drawer layout
+ */
+ private int mMaxWidth;
+
+ /**
+ * Max total visible height of views not marked always-show when in the closed/initial state
+ */
+ private int mMaxCollapsedHeight;
+
+ /**
+ * Max total visible height of views not marked always-show when in the closed/initial state
+ * when a default option is present
+ */
+ private int mMaxCollapsedHeightSmall;
+
+ /**
+ * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
+ * inferred by {@code mMaxCollapsedHeight}.
+ */
+ private final boolean mIsMaxCollapsedHeightSmallExplicit;
+
+ private boolean mSmallCollapsed;
+
+ /**
+ * Move views down from the top by this much in px
+ */
+ private float mCollapseOffset;
+
+ /**
+ * Track fractions of pixels from drag calculations. Without this, the view offsets get
+ * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
+ */
+ private float mDragRemainder = 0.0f;
+ private int mCollapsibleHeight;
+ private int mUncollapsibleHeight;
+ private int mAlwaysShowHeight;
+
+ /**
+ * The height in pixels of reserved space added to the top of the collapsed UI;
+ * e.g. chooser targets
+ */
+ private int mCollapsibleHeightReserved;
+
+ private int mTopOffset;
+ private boolean mShowAtTop;
+
+ private boolean mIsDragging;
+ private boolean mOpenOnClick;
+ private boolean mOpenOnLayout;
+ private boolean mDismissOnScrollerFinished;
+ private final int mTouchSlop;
+ private final float mMinFlingVelocity;
+ private final OverScroller mScroller;
+ private final VelocityTracker mVelocityTracker;
+
+ private Drawable mScrollIndicatorDrawable;
+
+ private OnDismissedListener mOnDismissedListener;
+ private RunOnDismissedListener mRunOnDismissedListener;
+ private OnCollapsedChangedListener mOnCollapsedChangedListener;
+
+ private boolean mDismissLocked;
+
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private float mLastTouchY;
+ private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+ private final Rect mTempRect = new Rect();
+
+ private AbsListView mNestedListChild;
+ private RecyclerView mNestedRecyclerChild;
+
+ private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
+ new ViewTreeObserver.OnTouchModeChangeListener() {
+ @Override
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
+ smoothScrollTo(0, 0);
+ }
+ }
+ };
+
+ public ResolverDrawerLayout(Context context) {
+ this(context, null);
+ }
+
+ public ResolverDrawerLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
+ defStyleAttr, 0);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1);
+ mMaxCollapsedHeight = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
+ mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
+ mMaxCollapsedHeight);
+ mIsMaxCollapsedHeightSmallExplicit =
+ a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
+ mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
+ a.recycle();
+
+ mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material);
+
+ mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
+ android.R.interpolator.decelerate_quint));
+ mVelocityTracker = VelocityTracker.obtain();
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ /**
+ * Dynamically set the max collapsed height. Note this also updates the small collapsed
+ * height if it wasn't specified explicitly.
+ */
+ public void setMaxCollapsedHeight(int heightInPixels) {
+ if (heightInPixels == mMaxCollapsedHeight) {
+ return;
+ }
+ mMaxCollapsedHeight = heightInPixels;
+ if (!mIsMaxCollapsedHeightSmallExplicit) {
+ mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
+ }
+ requestLayout();
+ }
+
+ public void setSmallCollapsed(boolean smallCollapsed) {
+ mSmallCollapsed = smallCollapsed;
+ requestLayout();
+ }
+
+ public boolean isSmallCollapsed() {
+ return mSmallCollapsed;
+ }
+
+ public boolean isCollapsed() {
+ return mCollapseOffset > 0;
+ }
+
+ public void setShowAtTop(boolean showOnTop) {
+ mShowAtTop = showOnTop;
+ invalidate();
+ requestLayout();
+ }
+
+ public boolean getShowAtTop() {
+ return mShowAtTop;
+ }
+
+ public void setCollapsed(boolean collapsed) {
+ if (!isLaidOut()) {
+ mOpenOnLayout = !collapsed;
+ } else {
+ smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
+ }
+ }
+
+ public void setCollapsibleHeightReserved(int heightPixels) {
+ final int oldReserved = mCollapsibleHeightReserved;
+ mCollapsibleHeightReserved = heightPixels;
+
+ final int dReserved = mCollapsibleHeightReserved - oldReserved;
+ if (dReserved != 0 && mIsDragging) {
+ mLastTouchY -= dReserved;
+ }
+
+ final int oldCollapsibleHeight = mCollapsibleHeight;
+ mCollapsibleHeight = Math.min(mCollapsibleHeight, getMaxCollapsedHeight());
+
+ if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
+ return;
+ }
+
+ invalidate();
+ }
+
+ public void setDismissLocked(boolean locked) {
+ mDismissLocked = locked;
+ }
+
+ private boolean isMoving() {
+ return mIsDragging || !mScroller.isFinished();
+ }
+
+ private boolean isDragging() {
+ return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
+ }
+
+ private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
+ if (oldCollapsibleHeight == mCollapsibleHeight) {
+ return false;
+ }
+
+ if (getShowAtTop()) {
+ // Keep the drawer fully open.
+ mCollapseOffset = 0;
+ return false;
+ }
+
+ if (isLaidOut()) {
+ final boolean isCollapsedOld = mCollapseOffset != 0;
+ if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
+ && mCollapseOffset == oldCollapsibleHeight)) {
+ // Stay closed even at the new height.
+ mCollapseOffset = mCollapsibleHeight;
+ } else {
+ mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
+ }
+ final boolean isCollapsedNew = mCollapseOffset != 0;
+ if (isCollapsedOld != isCollapsedNew) {
+ onCollapsedChanged(isCollapsedNew);
+ }
+ } else {
+ // Start out collapsed at first unless we restored state for otherwise
+ mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;
+ }
+ return true;
+ }
+
+ private int getMaxCollapsedHeight() {
+ return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
+ + mCollapsibleHeightReserved;
+ }
+
+ public void setOnDismissedListener(OnDismissedListener listener) {
+ mOnDismissedListener = listener;
+ }
+
+ private boolean isDismissable() {
+ return mOnDismissedListener != null && !mDismissLocked;
+ }
+
+ public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
+ mOnCollapsedChangedListener = listener;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mVelocityTracker.clear();
+ }
+
+ mVelocityTracker.addMovement(ev);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialTouchX = x;
+ mInitialTouchY = mLastTouchY = y;
+ mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final float dy = y - mInitialTouchY;
+ if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
+ (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+ mActivePointerId = ev.getPointerId(0);
+ mIsDragging = true;
+ mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+ Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ resetTouch();
+ }
+ break;
+ }
+
+ if (mIsDragging) {
+ abortAnimation();
+ }
+ return mIsDragging || mOpenOnClick;
+ }
+
+ private boolean isNestedListChildScrolled() {
+ return mNestedListChild != null
+ && mNestedListChild.getChildCount() > 0
+ && (mNestedListChild.getFirstVisiblePosition() > 0
+ || mNestedListChild.getChildAt(0).getTop() < 0);
+ }
+
+ private boolean isNestedRecyclerChildScrolled() {
+ if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
+ final RecyclerView.ViewHolder vh =
+ mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
+ return vh == null || vh.itemView.getTop() < 0;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ mVelocityTracker.addMovement(ev);
+
+ boolean handled = false;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialTouchX = x;
+ mInitialTouchY = mLastTouchY = y;
+ mActivePointerId = ev.getPointerId(0);
+ final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
+ handled = isDismissable() || mCollapsibleHeight > 0;
+ mIsDragging = hitView && handled;
+ abortAnimation();
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ int index = ev.findPointerIndex(mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
+ index = 0;
+ mActivePointerId = ev.getPointerId(0);
+ mInitialTouchX = ev.getX();
+ mInitialTouchY = mLastTouchY = ev.getY();
+ }
+ final float x = ev.getX(index);
+ final float y = ev.getY(index);
+ if (!mIsDragging) {
+ final float dy = y - mInitialTouchY;
+ if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
+ handled = mIsDragging = true;
+ mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+ Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+ }
+ }
+ if (mIsDragging) {
+ final float dy = y - mLastTouchY;
+ if (dy > 0 && isNestedListChildScrolled()) {
+ mNestedListChild.smoothScrollBy((int) -dy, 0);
+ } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
+ mNestedRecyclerChild.scrollBy(0, (int) -dy);
+ } else {
+ performDrag(dy);
+ }
+ }
+ mLastTouchY = y;
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ mActivePointerId = pointerId;
+ mInitialTouchX = ev.getX(pointerIndex);
+ mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ final boolean wasDragging = mIsDragging;
+ mIsDragging = false;
+ if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
+ findChildUnder(ev.getX(), ev.getY()) == null) {
+ if (isDismissable()) {
+ dispatchOnDismissed();
+ resetTouch();
+ return true;
+ }
+ }
+ if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
+ Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
+ smoothScrollTo(0, 0);
+ return true;
+ }
+ mVelocityTracker.computeCurrentVelocity(1000);
+ final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
+ if (Math.abs(yvel) > mMinFlingVelocity) {
+ if (getShowAtTop()) {
+ if (isDismissable() && yvel < 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
+ } else {
+ if (isDismissable()
+ && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
+ mDismissOnScrollerFinished = true;
+ } else {
+ if (isNestedListChildScrolled()) {
+ mNestedListChild.smoothScrollToPosition(0);
+ } else if (isNestedRecyclerChildScrolled()) {
+ mNestedRecyclerChild.smoothScrollToPosition(0);
+ }
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
+ }
+ }else {
+ smoothScrollTo(
+ mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ resetTouch();
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ if (mIsDragging) {
+ smoothScrollTo(
+ mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ resetTouch();
+ return true;
+ }
+ }
+
+ return handled;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mInitialTouchX = ev.getX(newPointerIndex);
+ mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ }
+ }
+
+ private void resetTouch() {
+ mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+ mIsDragging = false;
+ mOpenOnClick = false;
+ mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
+ mVelocityTracker.clear();
+ }
+
+ private void dismiss() {
+ mRunOnDismissedListener = new RunOnDismissedListener();
+ post(mRunOnDismissedListener);
+ }
+
+ @Override
+ public void computeScroll() {
+ super.computeScroll();
+ if (mScroller.computeScrollOffset()) {
+ final boolean keepGoing = !mScroller.isFinished();
+ performDrag(mScroller.getCurrY() - mCollapseOffset);
+ if (keepGoing) {
+ postInvalidateOnAnimation();
+ } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
+ dismiss();
+ }
+ }
+ }
+
+ private void abortAnimation() {
+ mScroller.abortAnimation();
+ mRunOnDismissedListener = null;
+ mDismissOnScrollerFinished = false;
+ }
+
+ private float performDrag(float dy) {
+ if (getShowAtTop()) {
+ return 0;
+ }
+
+ final float newPos = Math.max(0, Math.min(mCollapseOffset + dy,
+ mCollapsibleHeight + mUncollapsibleHeight));
+ if (newPos != mCollapseOffset) {
+ dy = newPos - mCollapseOffset;
+
+ mDragRemainder += dy - (int) dy;
+ if (mDragRemainder >= 1.0f) {
+ mDragRemainder -= 1.0f;
+ dy += 1.0f;
+ } else if (mDragRemainder <= -1.0f) {
+ mDragRemainder += 1.0f;
+ dy -= 1.0f;
+ }
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.ignoreOffset) {
+ child.offsetTopAndBottom((int) dy);
+ }
+ }
+ final boolean isCollapsedOld = mCollapseOffset != 0;
+ mCollapseOffset = newPos;
+ mTopOffset += dy;
+ final boolean isCollapsedNew = newPos != 0;
+ if (isCollapsedOld != isCollapsedNew) {
+ onCollapsedChanged(isCollapsedNew);
+ getMetricsLogger().write(
+ new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED)
+ .setSubtype(isCollapsedNew ? 1 : 0));
+ }
+ onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
+ postInvalidateOnAnimation();
+ return dy;
+ }
+ return 0;
+ }
+
+ private void onCollapsedChanged(boolean isCollapsed) {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+
+ if (mScrollIndicatorDrawable != null) {
+ setWillNotDraw(!isCollapsed);
+ }
+
+ if (mOnCollapsedChangedListener != null) {
+ mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
+ }
+ }
+
+ void dispatchOnDismissed() {
+ if (mOnDismissedListener != null) {
+ mOnDismissedListener.onDismissed();
+ }
+ if (mRunOnDismissedListener != null) {
+ removeCallbacks(mRunOnDismissedListener);
+ mRunOnDismissedListener = null;
+ }
+ }
+
+ private void smoothScrollTo(int yOffset, float velocity) {
+ abortAnimation();
+ final int sy = (int) mCollapseOffset;
+ int dy = yOffset - sy;
+ if (dy == 0) {
+ return;
+ }
+
+ final int height = getHeight();
+ final int halfHeight = height / 2;
+ final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
+ final float distance = halfHeight + halfHeight *
+ distanceInfluenceForSnapDuration(distanceRatio);
+
+ int duration = 0;
+ velocity = Math.abs(velocity);
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ final float pageDelta = (float) Math.abs(dy) / height;
+ duration = (int) ((pageDelta + 1) * 100);
+ }
+ duration = Math.min(duration, 300);
+
+ mScroller.startScroll(0, sy, 0, dy, duration);
+ postInvalidateOnAnimation();
+ }
+
+ private float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ /**
+ * Note: this method doesn't take Z into account for overlapping views
+ * since it is only used in contexts where this doesn't affect the outcome.
+ */
+ private View findChildUnder(float x, float y) {
+ return findChildUnder(this, x, y);
+ }
+
+ private static View findChildUnder(ViewGroup parent, float x, float y) {
+ final int childCount = parent.getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = parent.getChildAt(i);
+ if (isChildUnder(child, x, y)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private View findListChildUnder(float x, float y) {
+ View v = findChildUnder(x, y);
+ while (v != null) {
+ x -= v.getX();
+ y -= v.getY();
+ if (v instanceof AbsListView) {
+ // One more after this.
+ return findChildUnder((ViewGroup) v, x, y);
+ }
+ v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
+ }
+ return v;
+ }
+
+ /**
+ * This only checks clipping along the bottom edge.
+ */
+ private boolean isListChildUnderClipped(float x, float y) {
+ final View listChild = findListChildUnder(x, y);
+ return listChild != null && isDescendantClipped(listChild);
+ }
+
+ private boolean isDescendantClipped(View child) {
+ mTempRect.set(0, 0, child.getWidth(), child.getHeight());
+ offsetDescendantRectToMyCoords(child, mTempRect);
+ View directChild;
+ if (child.getParent() == this) {
+ directChild = child;
+ } else {
+ View v = child;
+ ViewParent p = child.getParent();
+ while (p != this) {
+ v = (View) p;
+ p = v.getParent();
+ }
+ directChild = v;
+ }
+
+ // ResolverDrawerLayout lays out vertically in child order;
+ // the next view and forward is what to check against.
+ int clipEdge = getHeight() - getPaddingBottom();
+ final int childCount = getChildCount();
+ for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
+ final View nextChild = getChildAt(i);
+ if (nextChild.getVisibility() == GONE) {
+ continue;
+ }
+ clipEdge = Math.min(clipEdge, nextChild.getTop());
+ }
+ return mTempRect.bottom > clipEdge;
+ }
+
+ private static boolean isChildUnder(View child, float x, float y) {
+ final float left = child.getX();
+ final float top = child.getY();
+ final float right = left + child.getWidth();
+ final float bottom = top + child.getHeight();
+ return x >= left && y >= top && x < right && y < bottom;
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ super.requestChildFocus(child, focused);
+ if (!isInTouchMode() && isDescendantClipped(focused)) {
+ smoothScrollTo(0, 0);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
+ abortAnimation();
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
+ if (target instanceof AbsListView) {
+ mNestedListChild = (AbsListView) target;
+ }
+ if (target instanceof RecyclerView) {
+ mNestedRecyclerChild = (RecyclerView) target;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ super.onNestedScrollAccepted(child, target, axes);
+ }
+
+ @Override
+ public void onStopNestedScroll(View child) {
+ super.onStopNestedScroll(child);
+ if (mScroller.isFinished()) {
+ smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ if (dyUnconsumed < 0) {
+ performDrag(-dyUnconsumed);
+ }
+ }
+
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ if (dy > 0) {
+ consumed[1] = (int) -performDrag(-dy);
+ }
+ }
+
+ @Override
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
+ smoothScrollTo(0, velocityY);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
+ if (getShowAtTop()) {
+ if (isDismissable() && velocityY > 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
+ }
+ } else {
+ if (isDismissable()
+ && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
+ mDismissOnScrollerFinished = true;
+ } else {
+ smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean performAccessibilityActionCommon(int action) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case AccessibilityNodeInfo.ACTION_EXPAND:
+ case R.id.accessibilityActionScrollDown:
+ if (mCollapseOffset != 0) {
+ smoothScrollTo(0, 0);
+ return true;
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ case R.id.accessibilityActionScrollUp:
+ if (mCollapseOffset < mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight, 0);
+ return true;
+ } else if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
+ && isDismissable()) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0);
+ mDismissOnScrollerFinished = true;
+ return true;
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_COLLAPSE:
+ if (mCollapseOffset < mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight, 0);
+ return true;
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_DISMISS:
+ if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
+ && isDismissable()) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0);
+ mDismissOnScrollerFinished = true;
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+ if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
+ return true;
+ }
+
+ return performAccessibilityActionCommon(action);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ // Since we support scrolling, make this ViewGroup look like a
+ // ScrollView. This is kind of a hack until we have support for
+ // specifying auto-scroll behavior.
+ return android.widget.ScrollView.class.getName();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (isEnabled()) {
+ if (mCollapseOffset != 0) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityAction.ACTION_EXPAND);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
+ info.setScrollable(true);
+ }
+ if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
+ && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
+ info.setScrollable(true);
+ }
+ if (mCollapseOffset < mCollapsibleHeight) {
+ info.addAction(AccessibilityAction.ACTION_COLLAPSE);
+ }
+ if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) {
+ info.addAction(AccessibilityAction.ACTION_DISMISS);
+ }
+ }
+
+ // This view should never get accessibility focus, but it's interactive
+ // via nested scrolling, so we can't hide it completely.
+ info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
+ // This view should never get accessibility focus.
+ return false;
+ }
+
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+
+ return performAccessibilityActionCommon(action);
+ }
+
+ @Override
+ public void onDrawForeground(Canvas canvas) {
+ if (mScrollIndicatorDrawable != null) {
+ mScrollIndicatorDrawable.draw(canvas);
+ }
+
+ super.onDrawForeground(canvas);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int widthSize = sourceWidth;
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Single-use layout; just ignore the mode and use available space.
+ // Clamp to maxWidth.
+ if (mMaxWidth >= 0) {
+ widthSize = Math.min(widthSize, mMaxWidth);
+ }
+
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+
+ // Currently we allot more height than is really needed so that the entirety of the
+ // sheet may be pulled up.
+ // TODO: Restrict the height here to be the right value.
+ int heightUsed = 0;
+
+ // Measure always-show children first.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.alwaysShow && child.getVisibility() != GONE) {
+ if (lp.maxHeight != -1) {
+ final int remainingHeight = heightSize - heightUsed;
+ measureChildWithMargins(child, widthSpec, 0,
+ MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+ lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+ } else {
+ measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+ }
+ heightUsed += child.getMeasuredHeight();
+ }
+ }
+
+ mAlwaysShowHeight = heightUsed;
+
+ // And now the rest.
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.alwaysShow && child.getVisibility() != GONE) {
+ if (lp.maxHeight != -1) {
+ final int remainingHeight = heightSize - heightUsed;
+ measureChildWithMargins(child, widthSpec, 0,
+ MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+ lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+ } else {
+ measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+ }
+ heightUsed += child.getMeasuredHeight();
+ }
+ }
+
+ final int oldCollapsibleHeight = mCollapsibleHeight;
+ mCollapsibleHeight = Math.max(0,
+ heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
+ mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
+
+ updateCollapseOffset(oldCollapsibleHeight, !isDragging());
+
+ if (getShowAtTop()) {
+ mTopOffset = 0;
+ } else {
+ mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
+ }
+
+ setMeasuredDimension(sourceWidth, heightSize);
+ }
+
+ /**
+ * @return The space reserved by views with 'alwaysShow=true'
+ */
+ public int getAlwaysShowHeight() {
+ return mAlwaysShowHeight;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int width = getWidth();
+
+ View indicatorHost = null;
+
+ int ypos = mTopOffset;
+ int leftEdge = getPaddingLeft();
+ int rightEdge = width - getPaddingRight();
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.hasNestedScrollIndicator) {
+ indicatorHost = child;
+ }
+
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ int top = ypos + lp.topMargin;
+ if (lp.ignoreOffset) {
+ top -= mCollapseOffset;
+ }
+ final int bottom = top + child.getMeasuredHeight();
+
+ final int childWidth = child.getMeasuredWidth();
+ final int widthAvailable = rightEdge - leftEdge;
+ final int left = leftEdge + (widthAvailable - childWidth) / 2;
+ final int right = left + childWidth;
+
+ child.layout(left, top, right, bottom);
+
+ ypos = bottom + lp.bottomMargin;
+ }
+
+ if (mScrollIndicatorDrawable != null) {
+ if (indicatorHost != null) {
+ final int left = indicatorHost.getLeft();
+ final int right = indicatorHost.getRight();
+ final int bottom = indicatorHost.getTop();
+ final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
+ mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
+ setWillNotDraw(!isCollapsed());
+ } else {
+ mScrollIndicatorDrawable = null;
+ setWillNotDraw(true);
+ }
+ }
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (p instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) p);
+ } else if (p instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) p);
+ }
+ return new LayoutParams(p);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final SavedState ss = new SavedState(super.onSaveInstanceState());
+ ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
+ ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mOpenOnLayout = ss.open;
+ mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
+ }
+
+ public static class LayoutParams extends MarginLayoutParams {
+ public boolean alwaysShow;
+ public boolean ignoreOffset;
+ public boolean hasNestedScrollIndicator;
+ public int maxHeight;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ final TypedArray a = c.obtainStyledAttributes(attrs,
+ R.styleable.ResolverDrawerLayout_LayoutParams);
+ alwaysShow = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
+ false);
+ ignoreOffset = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
+ false);
+ hasNestedScrollIndicator = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
+ false);
+ maxHeight = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
+ a.recycle();
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ this.alwaysShow = source.alwaysShow;
+ this.ignoreOffset = source.ignoreOffset;
+ this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
+ this.maxHeight = source.maxHeight;
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean open;
+ private int mCollapsibleHeightReserved;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ open = in.readInt() != 0;
+ mCollapsibleHeightReserved = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(open ? 1 : 0);
+ out.writeInt(mCollapsibleHeightReserved);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ /**
+ * Listener for sheet dismissed events.
+ */
+ public interface OnDismissedListener {
+ /**
+ * Callback when the sheet is dismissed by the user.
+ */
+ void onDismissed();
+ }
+
+ /**
+ * Listener for sheet collapsed / expanded events.
+ */
+ public interface OnCollapsedChangedListener {
+ /**
+ * Callback when the sheet is either fully expanded or collapsed.
+ * @param isCollapsed true when collapsed, false when expanded.
+ */
+ void onCollapsedChanged(boolean isCollapsed);
+ }
+
+ private class RunOnDismissedListener implements Runnable {
+ @Override
+ public void run() {
+ dispatchOnDismissed();
+ }
+ }
+
+ private MetricsLogger getMetricsLogger() {
+ if (mMetricsLogger == null) {
+ mMetricsLogger = new MetricsLogger();
+ }
+ return mMetricsLogger;
+ }
+}
diff --git a/com/android/internal/widget/ScrollBarUtils.java b/com/android/internal/widget/ScrollBarUtils.java
new file mode 100644
index 0000000..3e9d697
--- /dev/null
+++ b/com/android/internal/widget/ScrollBarUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+public class ScrollBarUtils {
+
+ @UnsupportedAppUsage
+ public static int getThumbLength(int size, int thickness, int extent, int range) {
+ // Avoid the tiny thumb.
+ final int minLength = thickness * 2;
+ int length = Math.round((float) size * extent / range);
+ if (length < minLength) {
+ length = minLength;
+ }
+ return length;
+ }
+
+ public static int getThumbOffset(int size, int thumbLength, int extent, int range, int offset) {
+ // Avoid the too-big thumb.
+ int thumbOffset = Math.round((float) (size - thumbLength) * offset / (range - extent));
+ if (thumbOffset > size - thumbLength) {
+ thumbOffset = size - thumbLength;
+ }
+ return thumbOffset;
+ }
+}
diff --git a/com/android/internal/widget/ScrollbarHelper.java b/com/android/internal/widget/ScrollbarHelper.java
new file mode 100644
index 0000000..ae34e4c
--- /dev/null
+++ b/com/android/internal/widget/ScrollbarHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.view.View;
+
+/**
+ * A helper class to do scroll offset calculations.
+ */
+class ScrollbarHelper {
+
+ /**
+ * @param startChild View closest to start of the list. (top or left)
+ * @param endChild View closest to end of the list (bottom or right)
+ */
+ static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
+ View startChild, View endChild, RecyclerView.LayoutManager lm,
+ boolean smoothScrollbarEnabled, boolean reverseLayout) {
+ if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
+ || endChild == null) {
+ return 0;
+ }
+ final int minPosition = Math.min(lm.getPosition(startChild),
+ lm.getPosition(endChild));
+ final int maxPosition = Math.max(lm.getPosition(startChild),
+ lm.getPosition(endChild));
+ final int itemsBefore = reverseLayout
+ ? Math.max(0, state.getItemCount() - maxPosition - 1)
+ : Math.max(0, minPosition);
+ if (!smoothScrollbarEnabled) {
+ return itemsBefore;
+ }
+ final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
+ - orientation.getDecoratedStart(startChild));
+ final int itemRange = Math.abs(lm.getPosition(startChild)
+ - lm.getPosition(endChild)) + 1;
+ final float avgSizePerRow = (float) laidOutArea / itemRange;
+
+ return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
+ - orientation.getDecoratedStart(startChild)));
+ }
+
+ /**
+ * @param startChild View closest to start of the list. (top or left)
+ * @param endChild View closest to end of the list (bottom or right)
+ */
+ static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
+ View startChild, View endChild, RecyclerView.LayoutManager lm,
+ boolean smoothScrollbarEnabled) {
+ if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
+ || endChild == null) {
+ return 0;
+ }
+ if (!smoothScrollbarEnabled) {
+ return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
+ }
+ final int extend = orientation.getDecoratedEnd(endChild)
+ - orientation.getDecoratedStart(startChild);
+ return Math.min(orientation.getTotalSpace(), extend);
+ }
+
+ /**
+ * @param startChild View closest to start of the list. (top or left)
+ * @param endChild View closest to end of the list (bottom or right)
+ */
+ static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
+ View startChild, View endChild, RecyclerView.LayoutManager lm,
+ boolean smoothScrollbarEnabled) {
+ if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
+ || endChild == null) {
+ return 0;
+ }
+ if (!smoothScrollbarEnabled) {
+ return state.getItemCount();
+ }
+ // smooth scrollbar enabled. try to estimate better.
+ final int laidOutArea = orientation.getDecoratedEnd(endChild)
+ - orientation.getDecoratedStart(startChild);
+ final int laidOutRange = Math.abs(lm.getPosition(startChild)
+ - lm.getPosition(endChild))
+ + 1;
+ // estimate a size for full list.
+ return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
+ }
+}
diff --git a/com/android/internal/widget/ScrollingTabContainerView.java b/com/android/internal/widget/ScrollingTabContainerView.java
new file mode 100644
index 0000000..aa0b0bb
--- /dev/null
+++ b/com/android/internal/widget/ScrollingTabContainerView.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2011 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.app.ActionBar;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.android.internal.view.ActionBarPolicy;
+
+/**
+ * This widget implements the dynamic action bar tab behavior that can change
+ * across different configurations or circumstances.
+ */
+public class ScrollingTabContainerView extends HorizontalScrollView
+ implements AdapterView.OnItemClickListener {
+ private static final String TAG = "ScrollingTabContainerView";
+ Runnable mTabSelector;
+ private TabClickListener mTabClickListener;
+
+ private LinearLayout mTabLayout;
+ private Spinner mTabSpinner;
+ private boolean mAllowCollapse;
+
+ int mMaxTabWidth;
+ int mStackedTabMaxWidth;
+ private int mContentHeight;
+ private int mSelectedTabIndex;
+
+ protected Animator mVisibilityAnim;
+ protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener();
+
+ private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator();
+
+ private static final int FADE_DURATION = 200;
+
+ @UnsupportedAppUsage
+ public ScrollingTabContainerView(Context context) {
+ super(context);
+ setHorizontalScrollBarEnabled(false);
+
+ ActionBarPolicy abp = ActionBarPolicy.get(context);
+ setContentHeight(abp.getTabContainerHeight());
+ mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
+
+ mTabLayout = createTabLayout();
+ addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY;
+ setFillViewport(lockedExpanded);
+
+ final int childCount = mTabLayout.getChildCount();
+ if (childCount > 1 &&
+ (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) {
+ if (childCount > 2) {
+ mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f);
+ } else {
+ mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
+ }
+ mMaxTabWidth = Math.min(mMaxTabWidth, mStackedTabMaxWidth);
+ } else {
+ mMaxTabWidth = -1;
+ }
+
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
+
+ final boolean canCollapse = !lockedExpanded && mAllowCollapse;
+
+ if (canCollapse) {
+ // See if we should expand
+ mTabLayout.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec);
+ if (mTabLayout.getMeasuredWidth() > MeasureSpec.getSize(widthMeasureSpec)) {
+ performCollapse();
+ } else {
+ performExpand();
+ }
+ } else {
+ performExpand();
+ }
+
+ final int oldWidth = getMeasuredWidth();
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ final int newWidth = getMeasuredWidth();
+
+ if (lockedExpanded && oldWidth != newWidth) {
+ // Recenter the tab display if we're at a new (scrollable) size.
+ setTabSelected(mSelectedTabIndex);
+ }
+ }
+
+ /**
+ * Indicates whether this view is collapsed into a dropdown menu instead
+ * of traditional tabs.
+ * @return true if showing as a spinner
+ */
+ private boolean isCollapsed() {
+ return mTabSpinner != null && mTabSpinner.getParent() == this;
+ }
+
+ @UnsupportedAppUsage
+ public void setAllowCollapse(boolean allowCollapse) {
+ mAllowCollapse = allowCollapse;
+ }
+
+ private void performCollapse() {
+ if (isCollapsed()) return;
+
+ if (mTabSpinner == null) {
+ mTabSpinner = createSpinner();
+ }
+ removeView(mTabLayout);
+ addView(mTabSpinner, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ if (mTabSpinner.getAdapter() == null) {
+ final TabAdapter adapter = new TabAdapter(mContext);
+ adapter.setDropDownViewContext(mTabSpinner.getPopupContext());
+ mTabSpinner.setAdapter(adapter);
+ }
+ if (mTabSelector != null) {
+ removeCallbacks(mTabSelector);
+ mTabSelector = null;
+ }
+ mTabSpinner.setSelection(mSelectedTabIndex);
+ }
+
+ private boolean performExpand() {
+ if (!isCollapsed()) return false;
+
+ removeView(mTabSpinner);
+ addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ setTabSelected(mTabSpinner.getSelectedItemPosition());
+ return false;
+ }
+
+ @UnsupportedAppUsage
+ public void setTabSelected(int position) {
+ mSelectedTabIndex = position;
+ final int tabCount = mTabLayout.getChildCount();
+ for (int i = 0; i < tabCount; i++) {
+ final View child = mTabLayout.getChildAt(i);
+ final boolean isSelected = i == position;
+ child.setSelected(isSelected);
+ if (isSelected) {
+ animateToTab(position);
+ }
+ }
+ if (mTabSpinner != null && position >= 0) {
+ mTabSpinner.setSelection(position);
+ }
+ }
+
+ public void setContentHeight(int contentHeight) {
+ mContentHeight = contentHeight;
+ requestLayout();
+ }
+
+ private LinearLayout createTabLayout() {
+ final LinearLayout tabLayout = new LinearLayout(getContext(), null,
+ com.android.internal.R.attr.actionBarTabBarStyle);
+ tabLayout.setMeasureWithLargestChildEnabled(true);
+ tabLayout.setGravity(Gravity.CENTER);
+ tabLayout.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
+ return tabLayout;
+ }
+
+ private Spinner createSpinner() {
+ final Spinner spinner = new Spinner(getContext(), null,
+ com.android.internal.R.attr.actionDropDownStyle);
+ spinner.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
+ spinner.setOnItemClickListenerInt(this);
+ return spinner;
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ ActionBarPolicy abp = ActionBarPolicy.get(getContext());
+ // Action bar can change size on configuration changes.
+ // Reread the desired height from the theme-specified style.
+ setContentHeight(abp.getTabContainerHeight());
+ mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
+ }
+
+ @UnsupportedAppUsage
+ public void animateToVisibility(int visibility) {
+ if (mVisibilityAnim != null) {
+ mVisibilityAnim.cancel();
+ }
+ if (visibility == VISIBLE) {
+ if (getVisibility() != VISIBLE) {
+ setAlpha(0);
+ }
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1);
+ anim.setDuration(FADE_DURATION);
+ anim.setInterpolator(sAlphaInterpolator);
+
+ anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+ anim.start();
+ } else {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0);
+ anim.setDuration(FADE_DURATION);
+ anim.setInterpolator(sAlphaInterpolator);
+
+ anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
+ anim.start();
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void animateToTab(final int position) {
+ final View tabView = mTabLayout.getChildAt(position);
+ if (mTabSelector != null) {
+ removeCallbacks(mTabSelector);
+ }
+ mTabSelector = new Runnable() {
+ public void run() {
+ final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2;
+ smoothScrollTo(scrollPos, 0);
+ mTabSelector = null;
+ }
+ };
+ post(mTabSelector);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mTabSelector != null) {
+ // Re-post the selector we saved
+ post(mTabSelector);
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mTabSelector != null) {
+ removeCallbacks(mTabSelector);
+ }
+ }
+
+ private TabView createTabView(Context context, ActionBar.Tab tab, boolean forAdapter) {
+ final TabView tabView = new TabView(context, tab, forAdapter);
+ if (forAdapter) {
+ tabView.setBackgroundDrawable(null);
+ tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT,
+ mContentHeight));
+ } else {
+ tabView.setFocusable(true);
+
+ if (mTabClickListener == null) {
+ mTabClickListener = new TabClickListener();
+ }
+ tabView.setOnClickListener(mTabClickListener);
+ }
+ return tabView;
+ }
+
+ @UnsupportedAppUsage
+ public void addTab(ActionBar.Tab tab, boolean setSelected) {
+ TabView tabView = createTabView(mContext, tab, false);
+ mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0,
+ LayoutParams.MATCH_PARENT, 1));
+ if (mTabSpinner != null) {
+ ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+ }
+ if (setSelected) {
+ tabView.setSelected(true);
+ }
+ if (mAllowCollapse) {
+ requestLayout();
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void addTab(ActionBar.Tab tab, int position, boolean setSelected) {
+ final TabView tabView = createTabView(mContext, tab, false);
+ mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams(
+ 0, LayoutParams.MATCH_PARENT, 1));
+ if (mTabSpinner != null) {
+ ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+ }
+ if (setSelected) {
+ tabView.setSelected(true);
+ }
+ if (mAllowCollapse) {
+ requestLayout();
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void updateTab(int position) {
+ ((TabView) mTabLayout.getChildAt(position)).update();
+ if (mTabSpinner != null) {
+ ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+ }
+ if (mAllowCollapse) {
+ requestLayout();
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void removeTabAt(int position) {
+ mTabLayout.removeViewAt(position);
+ if (mTabSpinner != null) {
+ ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+ }
+ if (mAllowCollapse) {
+ requestLayout();
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void removeAllTabs() {
+ mTabLayout.removeAllViews();
+ if (mTabSpinner != null) {
+ ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
+ }
+ if (mAllowCollapse) {
+ requestLayout();
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ TabView tabView = (TabView) view;
+ tabView.getTab().select();
+ }
+
+ private class TabView extends LinearLayout {
+ private ActionBar.Tab mTab;
+ private TextView mTextView;
+ private ImageView mIconView;
+ private View mCustomView;
+
+ public TabView(Context context, ActionBar.Tab tab, boolean forList) {
+ super(context, null, com.android.internal.R.attr.actionBarTabStyle);
+ mTab = tab;
+
+ if (forList) {
+ setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ }
+
+ update();
+ }
+
+ public void bindTab(ActionBar.Tab tab) {
+ mTab = tab;
+ update();
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ final boolean changed = (isSelected() != selected);
+ super.setSelected(selected);
+ if (changed && selected) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ // This view masquerades as an action bar tab.
+ return ActionBar.Tab.class.getName();
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ // Re-measure if we went beyond our maximum size.
+ if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) {
+ super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY),
+ heightMeasureSpec);
+ }
+ }
+
+ public void update() {
+ final ActionBar.Tab tab = mTab;
+ final View custom = tab.getCustomView();
+ if (custom != null) {
+ final ViewParent customParent = custom.getParent();
+ if (customParent != this) {
+ if (customParent != null) ((ViewGroup) customParent).removeView(custom);
+ addView(custom);
+ }
+ mCustomView = custom;
+ if (mTextView != null) mTextView.setVisibility(GONE);
+ if (mIconView != null) {
+ mIconView.setVisibility(GONE);
+ mIconView.setImageDrawable(null);
+ }
+ } else {
+ if (mCustomView != null) {
+ removeView(mCustomView);
+ mCustomView = null;
+ }
+
+ final Drawable icon = tab.getIcon();
+ final CharSequence text = tab.getText();
+
+ if (icon != null) {
+ if (mIconView == null) {
+ ImageView iconView = new ImageView(getContext());
+ LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_VERTICAL;
+ iconView.setLayoutParams(lp);
+ addView(iconView, 0);
+ mIconView = iconView;
+ }
+ mIconView.setImageDrawable(icon);
+ mIconView.setVisibility(VISIBLE);
+ } else if (mIconView != null) {
+ mIconView.setVisibility(GONE);
+ mIconView.setImageDrawable(null);
+ }
+
+ final boolean hasText = !TextUtils.isEmpty(text);
+ if (hasText) {
+ if (mTextView == null) {
+ TextView textView = new TextView(getContext(), null,
+ com.android.internal.R.attr.actionBarTabTextStyle);
+ textView.setEllipsize(TruncateAt.END);
+ LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_VERTICAL;
+ textView.setLayoutParams(lp);
+ addView(textView);
+ mTextView = textView;
+ }
+ mTextView.setText(text);
+ mTextView.setVisibility(VISIBLE);
+ } else if (mTextView != null) {
+ mTextView.setVisibility(GONE);
+ mTextView.setText(null);
+ }
+
+ if (mIconView != null) {
+ mIconView.setContentDescription(tab.getContentDescription());
+ }
+ setTooltipText(hasText? null : tab.getContentDescription());
+ }
+ }
+
+ public ActionBar.Tab getTab() {
+ return mTab;
+ }
+ }
+
+ private class TabAdapter extends BaseAdapter {
+ private Context mDropDownContext;
+
+ public TabAdapter(Context context) {
+ setDropDownViewContext(context);
+ }
+
+ public void setDropDownViewContext(Context context) {
+ mDropDownContext = context;
+ }
+
+ @Override
+ public int getCount() {
+ return mTabLayout.getChildCount();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return ((TabView) mTabLayout.getChildAt(position)).getTab();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = createTabView(mContext, (ActionBar.Tab) getItem(position), true);
+ } else {
+ ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
+ }
+ return convertView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = createTabView(mDropDownContext,
+ (ActionBar.Tab) getItem(position), true);
+ } else {
+ ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
+ }
+ return convertView;
+ }
+ }
+
+ private class TabClickListener implements OnClickListener {
+ public void onClick(View view) {
+ TabView tabView = (TabView) view;
+ tabView.getTab().select();
+ final int tabCount = mTabLayout.getChildCount();
+ for (int i = 0; i < tabCount; i++) {
+ final View child = mTabLayout.getChildAt(i);
+ child.setSelected(child == view);
+ }
+ }
+ }
+
+ protected class VisibilityAnimListener implements Animator.AnimatorListener {
+ private boolean mCanceled = false;
+ private int mFinalVisibility;
+
+ public VisibilityAnimListener withFinalVisibility(int visibility) {
+ mFinalVisibility = visibility;
+ return this;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setVisibility(VISIBLE);
+ mVisibilityAnim = animation;
+ mCanceled = false;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCanceled) return;
+
+ mVisibilityAnim = null;
+ setVisibility(mFinalVisibility);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCanceled = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ }
+}
diff --git a/com/android/internal/widget/ScrollingView.java b/com/android/internal/widget/ScrollingView.java
new file mode 100644
index 0000000..a0205e7
--- /dev/null
+++ b/com/android/internal/widget/ScrollingView.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+/**
+ * An interface that can be implemented by Views to provide scroll related APIs.
+ */
+public interface ScrollingView {
+ /**
+ * <p>Compute the horizontal range that the horizontal scrollbar
+ * represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollExtent()} and
+ * {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>The default range is the drawing width of this view.</p>
+ *
+ * @return the total horizontal range represented by the horizontal
+ * scrollbar
+ *
+ * @see #computeHorizontalScrollExtent()
+ * @see #computeHorizontalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ int computeHorizontalScrollRange();
+
+ /**
+ * <p>Compute the horizontal offset of the horizontal scrollbar's thumb
+ * within the horizontal range. This value is used to compute the position
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollRange()} and
+ * {@link #computeHorizontalScrollExtent()}.</p>
+ *
+ * <p>The default offset is the scroll offset of this view.</p>
+ *
+ * @return the horizontal offset of the scrollbar's thumb
+ *
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollExtent()
+ * @see android.widget.ScrollBarDrawable
+ */
+ int computeHorizontalScrollOffset();
+
+ /**
+ * <p>Compute the horizontal extent of the horizontal scrollbar's thumb
+ * within the horizontal range. This value is used to compute the length
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollRange()} and
+ * {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>The default extent is the drawing width of this view.</p>
+ *
+ * @return the horizontal extent of the scrollbar's thumb
+ *
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ int computeHorizontalScrollExtent();
+
+ /**
+ * <p>Compute the vertical range that the vertical scrollbar represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollExtent()} and
+ * {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * @return the total vertical range represented by the vertical scrollbar
+ *
+ * <p>The default range is the drawing height of this view.</p>
+ *
+ * @see #computeVerticalScrollExtent()
+ * @see #computeVerticalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ int computeVerticalScrollRange();
+
+ /**
+ * <p>Compute the vertical offset of the vertical scrollbar's thumb
+ * within the horizontal range. This value is used to compute the position
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollRange()} and
+ * {@link #computeVerticalScrollExtent()}.</p>
+ *
+ * <p>The default offset is the scroll offset of this view.</p>
+ *
+ * @return the vertical offset of the scrollbar's thumb
+ *
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollExtent()
+ * @see android.widget.ScrollBarDrawable
+ */
+ int computeVerticalScrollOffset();
+
+ /**
+ * <p>Compute the vertical extent of the vertical scrollbar's thumb
+ * within the vertical range. This value is used to compute the length
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollRange()} and
+ * {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * <p>The default extent is the drawing height of this view.</p>
+ *
+ * @return the vertical extent of the scrollbar's thumb
+ *
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ int computeVerticalScrollExtent();
+}
diff --git a/com/android/internal/widget/SimpleItemAnimator.java b/com/android/internal/widget/SimpleItemAnimator.java
new file mode 100644
index 0000000..f4cc753
--- /dev/null
+++ b/com/android/internal/widget/SimpleItemAnimator.java
@@ -0,0 +1,457 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+import android.view.View;
+
+import com.android.internal.widget.RecyclerView.Adapter;
+import com.android.internal.widget.RecyclerView.ViewHolder;
+
+/**
+ * A wrapper class for ItemAnimator that records View bounds and decides whether it should run
+ * move, change, add or remove animations. This class also replicates the original ItemAnimator
+ * API.
+ * <p>
+ * It uses {@link ItemHolderInfo} to track the bounds information of the Views. If you would like
+ * to
+ * extend this class, you can override {@link #obtainHolderInfo()} method to provide your own info
+ * class that extends {@link ItemHolderInfo}.
+ */
+public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
+
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "SimpleItemAnimator";
+
+ boolean mSupportsChangeAnimations = true;
+
+ /**
+ * Returns whether this ItemAnimator supports animations of change events.
+ *
+ * @return true if change animations are supported, false otherwise
+ */
+ @SuppressWarnings("unused")
+ public boolean getSupportsChangeAnimations() {
+ return mSupportsChangeAnimations;
+ }
+
+ /**
+ * Sets whether this ItemAnimator supports animations of item change events.
+ * If you set this property to false, actions on the data set which change the
+ * contents of items will not be animated. What those animations do is left
+ * up to the discretion of the ItemAnimator subclass, in its
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} implementation.
+ * The value of this property is true by default.
+ *
+ * @param supportsChangeAnimations true if change animations are supported by
+ * this ItemAnimator, false otherwise. If the property is false,
+ * the ItemAnimator
+ * will not receive a call to
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int,
+ * int)} when changes occur.
+ * @see Adapter#notifyItemChanged(int)
+ * @see Adapter#notifyItemRangeChanged(int, int)
+ */
+ public void setSupportsChangeAnimations(boolean supportsChangeAnimations) {
+ mSupportsChangeAnimations = supportsChangeAnimations;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return True if change animations are not supported or the ViewHolder is invalid,
+ * false otherwise.
+ *
+ * @see #setSupportsChangeAnimations(boolean)
+ */
+ @Override
+ public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
+ return !mSupportsChangeAnimations || viewHolder.isInvalid();
+ }
+
+ @Override
+ public boolean animateDisappearance(@NonNull ViewHolder viewHolder,
+ @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
+ int oldLeft = preLayoutInfo.left;
+ int oldTop = preLayoutInfo.top;
+ View disappearingItemView = viewHolder.itemView;
+ int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
+ int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
+ if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
+ disappearingItemView.layout(newLeft, newTop,
+ newLeft + disappearingItemView.getWidth(),
+ newTop + disappearingItemView.getHeight());
+ if (DEBUG) {
+ Log.d(TAG, "DISAPPEARING: " + viewHolder + " with view " + disappearingItemView);
+ }
+ return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "REMOVED: " + viewHolder + " with view " + disappearingItemView);
+ }
+ return animateRemove(viewHolder);
+ }
+ }
+
+ @Override
+ public boolean animateAppearance(@NonNull ViewHolder viewHolder,
+ @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
+ if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left
+ || preLayoutInfo.top != postLayoutInfo.top)) {
+ // slide items in if before/after locations differ
+ if (DEBUG) {
+ Log.d(TAG, "APPEARING: " + viewHolder + " with view " + viewHolder);
+ }
+ return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
+ postLayoutInfo.left, postLayoutInfo.top);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "ADDED: " + viewHolder + " with view " + viewHolder);
+ }
+ return animateAdd(viewHolder);
+ }
+ }
+
+ @Override
+ public boolean animatePersistence(@NonNull ViewHolder viewHolder,
+ @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
+ if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
+ if (DEBUG) {
+ Log.d(TAG, "PERSISTENT: " + viewHolder
+ + " with view " + viewHolder.itemView);
+ }
+ return animateMove(viewHolder,
+ preInfo.left, preInfo.top, postInfo.left, postInfo.top);
+ }
+ dispatchMoveFinished(viewHolder);
+ return false;
+ }
+
+ @Override
+ public boolean animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
+ @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
+ if (DEBUG) {
+ Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView);
+ }
+ final int fromLeft = preInfo.left;
+ final int fromTop = preInfo.top;
+ final int toLeft, toTop;
+ if (newHolder.shouldIgnore()) {
+ toLeft = preInfo.left;
+ toTop = preInfo.top;
+ } else {
+ toLeft = postInfo.left;
+ toTop = postInfo.top;
+ }
+ return animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop);
+ }
+
+ /**
+ * Called when an item is removed from the RecyclerView. Implementors can choose
+ * whether and how to animate that change, but must always call
+ * {@link #dispatchRemoveFinished(ViewHolder)} when done, either
+ * immediately (if no animation will occur) or after the animation actually finishes.
+ * The return value indicates whether an animation has been set up and whether the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+ * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+ * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+ * then start the animations together in the later call to {@link #runPendingAnimations()}.
+ *
+ * <p>This method may also be called for disappearing items which continue to exist in the
+ * RecyclerView, but for which the system does not have enough information to animate
+ * them out of view. In that case, the default animation for removing items is run
+ * on those items as well.</p>
+ *
+ * @param holder The item that is being removed.
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animateRemove(ViewHolder holder);
+
+ /**
+ * Called when an item is added to the RecyclerView. Implementors can choose
+ * whether and how to animate that change, but must always call
+ * {@link #dispatchAddFinished(ViewHolder)} when done, either
+ * immediately (if no animation will occur) or after the animation actually finishes.
+ * The return value indicates whether an animation has been set up and whether the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+ * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+ * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+ * then start the animations together in the later call to {@link #runPendingAnimations()}.
+ *
+ * <p>This method may also be called for appearing items which were already in the
+ * RecyclerView, but for which the system does not have enough information to animate
+ * them into view. In that case, the default animation for adding items is run
+ * on those items as well.</p>
+ *
+ * @param holder The item that is being added.
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animateAdd(ViewHolder holder);
+
+ /**
+ * Called when an item is moved in the RecyclerView. Implementors can choose
+ * whether and how to animate that change, but must always call
+ * {@link #dispatchMoveFinished(ViewHolder)} when done, either
+ * immediately (if no animation will occur) or after the animation actually finishes.
+ * The return value indicates whether an animation has been set up and whether the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+ * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+ * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+ * then start the animations together in the later call to {@link #runPendingAnimations()}.
+ *
+ * @param holder The item that is being moved.
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animateMove(ViewHolder holder, int fromX, int fromY,
+ int toX, int toY);
+
+ /**
+ * Called when an item is changed in the RecyclerView, as indicated by a call to
+ * {@link Adapter#notifyItemChanged(int)} or
+ * {@link Adapter#notifyItemRangeChanged(int, int)}.
+ * <p>
+ * Implementers can choose whether and how to animate changes, but must always call
+ * {@link #dispatchChangeFinished(ViewHolder, boolean)} for each non-null distinct ViewHolder,
+ * either immediately (if no animation will occur) or after the animation actually finishes.
+ * If the {@code oldHolder} is the same ViewHolder as the {@code newHolder}, you must call
+ * {@link #dispatchChangeFinished(ViewHolder, boolean)} once and only once. In that case, the
+ * second parameter of {@code dispatchChangeFinished} is ignored.
+ * <p>
+ * The return value indicates whether an animation has been set up and whether the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+ * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+ * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
+ * then start the animations together in the later call to {@link #runPendingAnimations()}.
+ *
+ * @param oldHolder The original item that changed.
+ * @param newHolder The new item that was created with the changed content. Might be null
+ * @param fromLeft Left of the old view holder
+ * @param fromTop Top of the old view holder
+ * @param toLeft Left of the new view holder
+ * @param toTop Top of the new view holder
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ public abstract boolean animateChange(ViewHolder oldHolder,
+ ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop);
+
+ /**
+ * Method to be called by subclasses when a remove animation is done.
+ *
+ * @param item The item which has been removed
+ * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo,
+ * ItemHolderInfo)
+ */
+ public final void dispatchRemoveFinished(ViewHolder item) {
+ onRemoveFinished(item);
+ dispatchAnimationFinished(item);
+ }
+
+ /**
+ * Method to be called by subclasses when a move animation is done.
+ *
+ * @param item The item which has been moved
+ * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo,
+ * ItemHolderInfo)
+ * @see RecyclerView.ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ * @see RecyclerView.ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
+ */
+ public final void dispatchMoveFinished(ViewHolder item) {
+ onMoveFinished(item);
+ dispatchAnimationFinished(item);
+ }
+
+ /**
+ * Method to be called by subclasses when an add animation is done.
+ *
+ * @param item The item which has been added
+ */
+ public final void dispatchAddFinished(ViewHolder item) {
+ onAddFinished(item);
+ dispatchAnimationFinished(item);
+ }
+
+ /**
+ * Method to be called by subclasses when a change animation is done.
+ *
+ * @param item The item which has been changed (this method must be called for
+ * each non-null ViewHolder passed into
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}).
+ * @param oldItem true if this is the old item that was changed, false if
+ * it is the new item that replaced the old item.
+ * @see #animateChange(ViewHolder, ViewHolder, int, int, int, int)
+ */
+ public final void dispatchChangeFinished(ViewHolder item, boolean oldItem) {
+ onChangeFinished(item, oldItem);
+ dispatchAnimationFinished(item);
+ }
+
+ /**
+ * Method to be called by subclasses when a remove animation is being started.
+ *
+ * @param item The item being removed
+ */
+ public final void dispatchRemoveStarting(ViewHolder item) {
+ onRemoveStarting(item);
+ }
+
+ /**
+ * Method to be called by subclasses when a move animation is being started.
+ *
+ * @param item The item being moved
+ */
+ public final void dispatchMoveStarting(ViewHolder item) {
+ onMoveStarting(item);
+ }
+
+ /**
+ * Method to be called by subclasses when an add animation is being started.
+ *
+ * @param item The item being added
+ */
+ public final void dispatchAddStarting(ViewHolder item) {
+ onAddStarting(item);
+ }
+
+ /**
+ * Method to be called by subclasses when a change animation is being started.
+ *
+ * @param item The item which has been changed (this method must be called for
+ * each non-null ViewHolder passed into
+ * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}).
+ * @param oldItem true if this is the old item that was changed, false if
+ * it is the new item that replaced the old item.
+ */
+ public final void dispatchChangeStarting(ViewHolder item, boolean oldItem) {
+ onChangeStarting(item, oldItem);
+ }
+
+ /**
+ * Called when a remove animation is being started on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ */
+ @SuppressWarnings("UnusedParameters")
+ public void onRemoveStarting(ViewHolder item) {
+ }
+
+ /**
+ * Called when a remove animation has ended on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ */
+ public void onRemoveFinished(ViewHolder item) {
+ }
+
+ /**
+ * Called when an add animation is being started on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ */
+ @SuppressWarnings("UnusedParameters")
+ public void onAddStarting(ViewHolder item) {
+ }
+
+ /**
+ * Called when an add animation has ended on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ */
+ public void onAddFinished(ViewHolder item) {
+ }
+
+ /**
+ * Called when a move animation is being started on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ */
+ @SuppressWarnings("UnusedParameters")
+ public void onMoveStarting(ViewHolder item) {
+ }
+
+ /**
+ * Called when a move animation has ended on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ */
+ public void onMoveFinished(ViewHolder item) {
+ }
+
+ /**
+ * Called when a change animation is being started on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ * @param oldItem true if this is the old item that was changed, false if
+ * it is the new item that replaced the old item.
+ */
+ @SuppressWarnings("UnusedParameters")
+ public void onChangeStarting(ViewHolder item, boolean oldItem) {
+ }
+
+ /**
+ * Called when a change animation has ended on the given ViewHolder.
+ * The default implementation does nothing. Subclasses may wish to override
+ * this method to handle any ViewHolder-specific operations linked to animation
+ * lifecycles.
+ *
+ * @param item The ViewHolder being animated.
+ * @param oldItem true if this is the old item that was changed, false if
+ * it is the new item that replaced the old item.
+ */
+ public void onChangeFinished(ViewHolder item, boolean oldItem) {
+ }
+}
+
diff --git a/com/android/internal/widget/SlidingTab.java b/com/android/internal/widget/SlidingTab.java
new file mode 100644
index 0000000..5e6f3a4
--- /dev/null
+++ b/com/android/internal/widget/SlidingTab.java
@@ -0,0 +1,897 @@
+/*
+ * Copyright (C) 2009 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.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.os.UserHandle;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.TranslateAnimation;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+/**
+ * A special widget containing two Sliders and a threshold for each. Moving either slider beyond
+ * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
+ * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
+ * Equivalently, selecting a tab will result in a call to
+ * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
+ * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
+ *
+ */
+public class SlidingTab extends ViewGroup {
+ private static final String LOG_TAG = "SlidingTab";
+ private static final boolean DBG = false;
+ private static final int HORIZONTAL = 0; // as defined in attrs.xml
+ private static final int VERTICAL = 1;
+
+ // TODO: Make these configurable
+ private static final float THRESHOLD = 2.0f / 3.0f;
+ private static final long VIBRATE_SHORT = 30;
+ private static final long VIBRATE_LONG = 40;
+ private static final int TRACKING_MARGIN = 50;
+ private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
+ private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
+ private boolean mHoldLeftOnTransition = true;
+ private boolean mHoldRightOnTransition = true;
+
+ private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+ .build();
+
+ private OnTriggerListener mOnTriggerListener;
+ private int mGrabbedState = OnTriggerListener.NO_HANDLE;
+ private boolean mTriggered = false;
+ private Vibrator mVibrator;
+ private final float mDensity; // used to scale dimensions for bitmaps.
+
+ /**
+ * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
+ */
+ private final int mOrientation;
+
+ @UnsupportedAppUsage
+ private final Slider mLeftSlider;
+ @UnsupportedAppUsage
+ private final Slider mRightSlider;
+ private Slider mCurrentSlider;
+ private boolean mTracking;
+ private float mThreshold;
+ private Slider mOtherSlider;
+ private boolean mAnimating;
+ private final Rect mTmpRect;
+
+ /**
+ * Listener used to reset the view when the current animation completes.
+ */
+ @UnsupportedAppUsage
+ private final AnimationListener mAnimationDoneListener = new AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+
+ }
+
+ public void onAnimationEnd(Animation animation) {
+ onAnimationDone();
+ }
+ };
+
+ /**
+ * Interface definition for a callback to be invoked when a tab is triggered
+ * by moving it beyond a threshold.
+ */
+ public interface OnTriggerListener {
+ /**
+ * The interface was triggered because the user let go of the handle without reaching the
+ * threshold.
+ */
+ public static final int NO_HANDLE = 0;
+
+ /**
+ * The interface was triggered because the user grabbed the left handle and moved it past
+ * the threshold.
+ */
+ public static final int LEFT_HANDLE = 1;
+
+ /**
+ * The interface was triggered because the user grabbed the right handle and moved it past
+ * the threshold.
+ */
+ public static final int RIGHT_HANDLE = 2;
+
+ /**
+ * Called when the user moves a handle beyond the threshold.
+ *
+ * @param v The view that was triggered.
+ * @param whichHandle Which "dial handle" the user grabbed,
+ * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
+ */
+ void onTrigger(View v, int whichHandle);
+
+ /**
+ * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
+ * one of the handles.)
+ *
+ * @param v the view that was triggered
+ * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
+ * or {@link #RIGHT_HANDLE}.
+ */
+ void onGrabbedStateChange(View v, int grabbedState);
+ }
+
+ /**
+ * Simple container class for all things pertinent to a slider.
+ * A slider consists of 3 Views:
+ *
+ * {@link #tab} is the tab shown on the screen in the default state.
+ * {@link #text} is the view revealed as the user slides the tab out.
+ * {@link #target} is the target the user must drag the slider past to trigger the slider.
+ *
+ */
+ private static class Slider {
+ /**
+ * Tab alignment - determines which side the tab should be drawn on
+ */
+ public static final int ALIGN_LEFT = 0;
+ public static final int ALIGN_RIGHT = 1;
+ public static final int ALIGN_TOP = 2;
+ public static final int ALIGN_BOTTOM = 3;
+ public static final int ALIGN_UNKNOWN = 4;
+
+ /**
+ * States for the view.
+ */
+ private static final int STATE_NORMAL = 0;
+ private static final int STATE_PRESSED = 1;
+ private static final int STATE_ACTIVE = 2;
+
+ @UnsupportedAppUsage
+ private final ImageView tab;
+ @UnsupportedAppUsage
+ private final TextView text;
+ private final ImageView target;
+ private int currentState = STATE_NORMAL;
+ private int alignment = ALIGN_UNKNOWN;
+ private int alignment_value;
+
+ /**
+ * Constructor
+ *
+ * @param parent the container view of this one
+ * @param tabId drawable for the tab
+ * @param barId drawable for the bar
+ * @param targetId drawable for the target
+ */
+ Slider(ViewGroup parent, int tabId, int barId, int targetId) {
+ // Create tab
+ tab = new ImageView(parent.getContext());
+ tab.setBackgroundResource(tabId);
+ tab.setScaleType(ScaleType.CENTER);
+ tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+
+ // Create hint TextView
+ text = new TextView(parent.getContext());
+ text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT));
+ text.setBackgroundResource(barId);
+ text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
+ // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen
+
+ // Create target
+ target = new ImageView(parent.getContext());
+ target.setImageResource(targetId);
+ target.setScaleType(ScaleType.CENTER);
+ target.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ target.setVisibility(View.INVISIBLE);
+
+ parent.addView(target); // this needs to be first - relies on painter's algorithm
+ parent.addView(tab);
+ parent.addView(text);
+ }
+
+ void setIcon(int iconId) {
+ tab.setImageResource(iconId);
+ }
+
+ void setTabBackgroundResource(int tabId) {
+ tab.setBackgroundResource(tabId);
+ }
+
+ void setBarBackgroundResource(int barId) {
+ text.setBackgroundResource(barId);
+ }
+
+ void setHintText(int resId) {
+ text.setText(resId);
+ }
+
+ void hide() {
+ boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+ int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
+ : alignment_value - tab.getLeft()) : 0;
+ int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
+ : alignment_value - tab.getTop());
+
+ Animation trans = new TranslateAnimation(0, dx, 0, dy);
+ trans.setDuration(ANIM_DURATION);
+ trans.setFillAfter(true);
+ tab.startAnimation(trans);
+ text.startAnimation(trans);
+ target.setVisibility(View.INVISIBLE);
+ }
+
+ void show(boolean animate) {
+ text.setVisibility(View.VISIBLE);
+ tab.setVisibility(View.VISIBLE);
+ //target.setVisibility(View.INVISIBLE);
+ if (animate) {
+ boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+ int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
+ int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
+
+ Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
+ trans.setDuration(ANIM_DURATION);
+ tab.startAnimation(trans);
+ text.startAnimation(trans);
+ }
+ }
+
+ void setState(int state) {
+ text.setPressed(state == STATE_PRESSED);
+ tab.setPressed(state == STATE_PRESSED);
+ if (state == STATE_ACTIVE) {
+ final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
+ if (text.getBackground().isStateful()) {
+ text.getBackground().setState(activeState);
+ }
+ if (tab.getBackground().isStateful()) {
+ tab.getBackground().setState(activeState);
+ }
+ text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
+ } else {
+ text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
+ }
+ currentState = state;
+ }
+
+ void showTarget() {
+ AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
+ alphaAnim.setDuration(ANIM_TARGET_TIME);
+ target.startAnimation(alphaAnim);
+ target.setVisibility(View.VISIBLE);
+ }
+
+ void reset(boolean animate) {
+ setState(STATE_NORMAL);
+ text.setVisibility(View.VISIBLE);
+ text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
+ tab.setVisibility(View.VISIBLE);
+ target.setVisibility(View.INVISIBLE);
+ final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
+ int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getLeft()
+ : alignment_value - tab.getRight()) : 0;
+ int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
+ : alignment_value - tab.getBottom());
+ if (animate) {
+ TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
+ trans.setDuration(ANIM_DURATION);
+ trans.setFillAfter(false);
+ text.startAnimation(trans);
+ tab.startAnimation(trans);
+ } else {
+ if (horiz) {
+ text.offsetLeftAndRight(dx);
+ tab.offsetLeftAndRight(dx);
+ } else {
+ text.offsetTopAndBottom(dy);
+ tab.offsetTopAndBottom(dy);
+ }
+ text.clearAnimation();
+ tab.clearAnimation();
+ target.clearAnimation();
+ }
+ }
+
+ void setTarget(int targetId) {
+ target.setImageResource(targetId);
+ }
+
+ /**
+ * Layout the given widgets within the parent.
+ *
+ * @param l the parent's left border
+ * @param t the parent's top border
+ * @param r the parent's right border
+ * @param b the parent's bottom border
+ * @param alignment which side to align the widget to
+ */
+ void layout(int l, int t, int r, int b, int alignment) {
+ this.alignment = alignment;
+ final Drawable tabBackground = tab.getBackground();
+ final int handleWidth = tabBackground.getIntrinsicWidth();
+ final int handleHeight = tabBackground.getIntrinsicHeight();
+ final Drawable targetDrawable = target.getDrawable();
+ final int targetWidth = targetDrawable.getIntrinsicWidth();
+ final int targetHeight = targetDrawable.getIntrinsicHeight();
+ final int parentWidth = r - l;
+ final int parentHeight = b - t;
+
+ final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
+ final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
+ final int left = (parentWidth - handleWidth) / 2;
+ final int right = left + handleWidth;
+
+ if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
+ // horizontal
+ final int targetTop = (parentHeight - targetHeight) / 2;
+ final int targetBottom = targetTop + targetHeight;
+ final int top = (parentHeight - handleHeight) / 2;
+ final int bottom = (parentHeight + handleHeight) / 2;
+ if (alignment == ALIGN_LEFT) {
+ tab.layout(0, top, handleWidth, bottom);
+ text.layout(0 - parentWidth, top, 0, bottom);
+ text.setGravity(Gravity.RIGHT);
+ target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
+ alignment_value = l;
+ } else {
+ tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
+ text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
+ target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
+ text.setGravity(Gravity.TOP);
+ alignment_value = r;
+ }
+ } else {
+ // vertical
+ final int targetLeft = (parentWidth - targetWidth) / 2;
+ final int targetRight = (parentWidth + targetWidth) / 2;
+ final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
+ final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
+ if (alignment == ALIGN_TOP) {
+ tab.layout(left, 0, right, handleHeight);
+ text.layout(left, 0 - parentHeight, right, 0);
+ target.layout(targetLeft, top, targetRight, top + targetHeight);
+ alignment_value = t;
+ } else {
+ tab.layout(left, parentHeight - handleHeight, right, parentHeight);
+ text.layout(left, parentHeight, right, parentHeight + parentHeight);
+ target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
+ alignment_value = b;
+ }
+ }
+ }
+
+ public void updateDrawableStates() {
+ setState(currentState);
+ }
+
+ /**
+ * Ensure all the dependent widgets are measured.
+ */
+ public void measure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ tab.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
+ text.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
+ }
+
+ /**
+ * Get the measured tab width. Must be called after {@link Slider#measure()}.
+ * @return
+ */
+ public int getTabWidth() {
+ return tab.getMeasuredWidth();
+ }
+
+ /**
+ * Get the measured tab width. Must be called after {@link Slider#measure()}.
+ * @return
+ */
+ public int getTabHeight() {
+ return tab.getMeasuredHeight();
+ }
+
+ /**
+ * Start animating the slider. Note we need two animations since a ValueAnimator
+ * keeps internal state of the invalidation region which is just the view being animated.
+ *
+ * @param anim1
+ * @param anim2
+ */
+ public void startAnimation(Animation anim1, Animation anim2) {
+ tab.startAnimation(anim1);
+ text.startAnimation(anim2);
+ }
+
+ public void hideTarget() {
+ target.clearAnimation();
+ target.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public SlidingTab(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructor used when this widget is created from a layout file.
+ */
+ public SlidingTab(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Allocate a temporary once that can be used everywhere.
+ mTmpRect = new Rect();
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
+ mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
+ a.recycle();
+
+ Resources r = getResources();
+ mDensity = r.getDisplayMetrics().density;
+ if (DBG) log("- Density: " + mDensity);
+
+ mLeftSlider = new Slider(this,
+ R.drawable.jog_tab_left_generic,
+ R.drawable.jog_tab_bar_left_generic,
+ R.drawable.jog_tab_target_gray);
+ mRightSlider = new Slider(this,
+ R.drawable.jog_tab_right_generic,
+ R.drawable.jog_tab_bar_right_generic,
+ R.drawable.jog_tab_target_gray);
+
+ // setBackgroundColor(0x80808080);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (DBG) {
+ if (widthSpecMode == MeasureSpec.UNSPECIFIED
+ || heightSpecMode == MeasureSpec.UNSPECIFIED) {
+ Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
+ +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
+ new RuntimeException(LOG_TAG + "stack:"));
+ }
+ }
+
+ mLeftSlider.measure(widthMeasureSpec, heightMeasureSpec);
+ mRightSlider.measure(widthMeasureSpec, heightMeasureSpec);
+ final int leftTabWidth = mLeftSlider.getTabWidth();
+ final int rightTabWidth = mRightSlider.getTabWidth();
+ final int leftTabHeight = mLeftSlider.getTabHeight();
+ final int rightTabHeight = mRightSlider.getTabHeight();
+ final int width;
+ final int height;
+ if (isHorizontal()) {
+ width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
+ height = Math.max(leftTabHeight, rightTabHeight);
+ } else {
+ width = Math.max(leftTabWidth, rightTabHeight);
+ height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ if (mAnimating) {
+ return false;
+ }
+
+ View leftHandle = mLeftSlider.tab;
+ leftHandle.getHitRect(mTmpRect);
+ boolean leftHit = mTmpRect.contains((int) x, (int) y);
+
+ View rightHandle = mRightSlider.tab;
+ rightHandle.getHitRect(mTmpRect);
+ boolean rightHit = mTmpRect.contains((int)x, (int) y);
+
+ if (!mTracking && !(leftHit || rightHit)) {
+ return false;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mTracking = true;
+ mTriggered = false;
+ vibrate(VIBRATE_SHORT);
+ if (leftHit) {
+ mCurrentSlider = mLeftSlider;
+ mOtherSlider = mRightSlider;
+ mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
+ setGrabbedState(OnTriggerListener.LEFT_HANDLE);
+ } else {
+ mCurrentSlider = mRightSlider;
+ mOtherSlider = mLeftSlider;
+ mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
+ setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
+ }
+ mCurrentSlider.setState(Slider.STATE_PRESSED);
+ mCurrentSlider.showTarget();
+ mOtherSlider.hide();
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Reset the tabs to their original state and stop any existing animation.
+ * Animate them back into place if animate is true.
+ *
+ * @param animate
+ */
+ public void reset(boolean animate) {
+ mLeftSlider.reset(animate);
+ mRightSlider.reset(animate);
+ if (!animate) {
+ mAnimating = false;
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ // Clear animations so sliders don't continue to animate when we show the widget again.
+ if (visibility != getVisibility() && visibility == View.INVISIBLE) {
+ reset(false);
+ }
+ super.setVisibility(visibility);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mTracking) {
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ if (withinView(x, y, this) ) {
+ moveHandle(x, y);
+ float position = isHorizontal() ? x : y;
+ float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
+ boolean thresholdReached;
+ if (isHorizontal()) {
+ thresholdReached = mCurrentSlider == mLeftSlider ?
+ position > target : position < target;
+ } else {
+ thresholdReached = mCurrentSlider == mLeftSlider ?
+ position < target : position > target;
+ }
+ if (!mTriggered && thresholdReached) {
+ mTriggered = true;
+ mTracking = false;
+ mCurrentSlider.setState(Slider.STATE_ACTIVE);
+ boolean isLeft = mCurrentSlider == mLeftSlider;
+ dispatchTriggerEvent(isLeft ?
+ OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
+
+ startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
+ setGrabbedState(OnTriggerListener.NO_HANDLE);
+ }
+ break;
+ }
+ // Intentionally fall through - we're outside tracking rectangle
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ cancelGrab();
+ break;
+ }
+ }
+
+ return mTracking || super.onTouchEvent(event);
+ }
+
+ private void cancelGrab() {
+ mTracking = false;
+ mTriggered = false;
+ mOtherSlider.show(true);
+ mCurrentSlider.reset(false);
+ mCurrentSlider.hideTarget();
+ mCurrentSlider = null;
+ mOtherSlider = null;
+ setGrabbedState(OnTriggerListener.NO_HANDLE);
+ }
+
+ void startAnimating(final boolean holdAfter) {
+ mAnimating = true;
+ final Animation trans1;
+ final Animation trans2;
+ final Slider slider = mCurrentSlider;
+ final Slider other = mOtherSlider;
+ final int dx;
+ final int dy;
+ if (isHorizontal()) {
+ int right = slider.tab.getRight();
+ int width = slider.tab.getWidth();
+ int left = slider.tab.getLeft();
+ int viewWidth = getWidth();
+ int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
+ dx = slider == mRightSlider ? - (right + viewWidth - holdOffset)
+ : (viewWidth - left) + viewWidth - holdOffset;
+ dy = 0;
+ } else {
+ int top = slider.tab.getTop();
+ int bottom = slider.tab.getBottom();
+ int height = slider.tab.getHeight();
+ int viewHeight = getHeight();
+ int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
+ dx = 0;
+ dy = slider == mRightSlider ? (top + viewHeight - holdOffset)
+ : - ((viewHeight - bottom) + viewHeight - holdOffset);
+ }
+ trans1 = new TranslateAnimation(0, dx, 0, dy);
+ trans1.setDuration(ANIM_DURATION);
+ trans1.setInterpolator(new LinearInterpolator());
+ trans1.setFillAfter(true);
+ trans2 = new TranslateAnimation(0, dx, 0, dy);
+ trans2.setDuration(ANIM_DURATION);
+ trans2.setInterpolator(new LinearInterpolator());
+ trans2.setFillAfter(true);
+
+ trans1.setAnimationListener(new AnimationListener() {
+ public void onAnimationEnd(Animation animation) {
+ Animation anim;
+ if (holdAfter) {
+ anim = new TranslateAnimation(dx, dx, dy, dy);
+ anim.setDuration(1000); // plenty of time for transitions
+ mAnimating = false;
+ } else {
+ anim = new AlphaAnimation(0.5f, 1.0f);
+ anim.setDuration(ANIM_DURATION);
+ resetView();
+ }
+ anim.setAnimationListener(mAnimationDoneListener);
+
+ /* Animation can be the same for these since the animation just holds */
+ mLeftSlider.startAnimation(anim, anim);
+ mRightSlider.startAnimation(anim, anim);
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+
+ }
+
+ public void onAnimationStart(Animation animation) {
+
+ }
+
+ });
+
+ slider.hideTarget();
+ slider.startAnimation(trans1, trans2);
+ }
+
+ @UnsupportedAppUsage
+ private void onAnimationDone() {
+ resetView();
+ mAnimating = false;
+ }
+
+ private boolean withinView(final float x, final float y, final View view) {
+ return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
+ || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
+ }
+
+ private boolean isHorizontal() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ @UnsupportedAppUsage
+ private void resetView() {
+ mLeftSlider.reset(false);
+ mRightSlider.reset(false);
+ // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (!changed) return;
+
+ // Center the widgets in the view
+ mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
+ mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
+ }
+
+ private void moveHandle(float x, float y) {
+ final View handle = mCurrentSlider.tab;
+ final View content = mCurrentSlider.text;
+ if (isHorizontal()) {
+ int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
+ handle.offsetLeftAndRight(deltaX);
+ content.offsetLeftAndRight(deltaX);
+ } else {
+ int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
+ handle.offsetTopAndBottom(deltaY);
+ content.offsetTopAndBottom(deltaY);
+ }
+ invalidate(); // TODO: be more conservative about what we're invalidating
+ }
+
+ /**
+ * Sets the left handle icon to a given resource.
+ *
+ * The resource should refer to a Drawable object, or use 0 to remove
+ * the icon.
+ *
+ * @param iconId the resource ID of the icon drawable
+ * @param targetId the resource of the target drawable
+ * @param barId the resource of the bar drawable (stateful)
+ * @param tabId the resource of the
+ */
+ @UnsupportedAppUsage
+ public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
+ mLeftSlider.setIcon(iconId);
+ mLeftSlider.setTarget(targetId);
+ mLeftSlider.setBarBackgroundResource(barId);
+ mLeftSlider.setTabBackgroundResource(tabId);
+ mLeftSlider.updateDrawableStates();
+ }
+
+ /**
+ * Sets the left handle hint text to a given resource string.
+ *
+ * @param resId
+ */
+ @UnsupportedAppUsage
+ public void setLeftHintText(int resId) {
+ if (isHorizontal()) {
+ mLeftSlider.setHintText(resId);
+ }
+ }
+
+ /**
+ * Sets the right handle icon to a given resource.
+ *
+ * The resource should refer to a Drawable object, or use 0 to remove
+ * the icon.
+ *
+ * @param iconId the resource ID of the icon drawable
+ * @param targetId the resource of the target drawable
+ * @param barId the resource of the bar drawable (stateful)
+ * @param tabId the resource of the
+ */
+ @UnsupportedAppUsage
+ public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
+ mRightSlider.setIcon(iconId);
+ mRightSlider.setTarget(targetId);
+ mRightSlider.setBarBackgroundResource(barId);
+ mRightSlider.setTabBackgroundResource(tabId);
+ mRightSlider.updateDrawableStates();
+ }
+
+ /**
+ * Sets the left handle hint text to a given resource string.
+ *
+ * @param resId
+ */
+ @UnsupportedAppUsage
+ public void setRightHintText(int resId) {
+ if (isHorizontal()) {
+ mRightSlider.setHintText(resId);
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
+ mHoldLeftOnTransition = holdLeft;
+ mHoldRightOnTransition = holdRight;
+ }
+
+ /**
+ * Triggers haptic feedback.
+ */
+ private synchronized void vibrate(long duration) {
+ final boolean hapticEnabled = Settings.System.getIntForUser(
+ mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
+ UserHandle.USER_CURRENT) != 0;
+ if (hapticEnabled) {
+ if (mVibrator == null) {
+ mVibrator = (android.os.Vibrator) getContext()
+ .getSystemService(Context.VIBRATOR_SERVICE);
+ }
+ mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES);
+ }
+ }
+
+ /**
+ * Registers a callback to be invoked when the user triggers an event.
+ *
+ * @param listener the OnDialTriggerListener to attach to this view
+ */
+ @UnsupportedAppUsage
+ public void setOnTriggerListener(OnTriggerListener listener) {
+ mOnTriggerListener = listener;
+ }
+
+ /**
+ * Dispatches a trigger event to listener. Ignored if a listener is not set.
+ * @param whichHandle the handle that triggered the event.
+ */
+ private void dispatchTriggerEvent(int whichHandle) {
+ vibrate(VIBRATE_LONG);
+ if (mOnTriggerListener != null) {
+ mOnTriggerListener.onTrigger(this, whichHandle);
+ }
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ // When visibility changes and the user has a tab selected, unselect it and
+ // make sure their callback gets called.
+ if (changedView == this && visibility != VISIBLE
+ && mGrabbedState != OnTriggerListener.NO_HANDLE) {
+ cancelGrab();
+ }
+ }
+
+ /**
+ * Sets the current grabbed state, and dispatches a grabbed state change
+ * event to our listener.
+ */
+ private void setGrabbedState(int newState) {
+ if (newState != mGrabbedState) {
+ mGrabbedState = newState;
+ if (mOnTriggerListener != null) {
+ mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
+ }
+ }
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/com/android/internal/widget/SubtitleView.java b/com/android/internal/widget/SubtitleView.java
new file mode 100644
index 0000000..21e63c5
--- /dev/null
+++ b/com/android/internal/widget/SubtitleView.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2013 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.internal.widget;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Join;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableStringBuilder;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+
+public class SubtitleView extends View {
+ // Ratio of inner padding to font size.
+ private static final float INNER_PADDING_RATIO = 0.125f;
+
+ /** Color used for the shadowed edge of a bevel. */
+ private static final int COLOR_BEVEL_DARK = 0x80000000;
+
+ /** Color used for the illuminated edge of a bevel. */
+ private static final int COLOR_BEVEL_LIGHT = 0x80FFFFFF;
+
+ // Styled dimensions.
+ private final float mCornerRadius;
+ private final float mOutlineWidth;
+ private final float mShadowRadius;
+ private final float mShadowOffsetX;
+ private final float mShadowOffsetY;
+
+ /** Temporary rectangle used for computing line bounds. */
+ private final RectF mLineBounds = new RectF();
+
+ /** Reusable spannable string builder used for holding text. */
+ private final SpannableStringBuilder mText = new SpannableStringBuilder();
+
+ private Alignment mAlignment = Alignment.ALIGN_CENTER;
+ private TextPaint mTextPaint;
+ private Paint mPaint;
+
+ private int mForegroundColor;
+ private int mBackgroundColor;
+ private int mEdgeColor;
+ private int mEdgeType;
+
+ private boolean mHasMeasurements;
+ private int mLastMeasuredWidth;
+ private StaticLayout mLayout;
+
+ private float mSpacingMult = 1;
+ private float mSpacingAdd = 0;
+ private int mInnerPaddingX = 0;
+
+ public SubtitleView(Context context) {
+ this(context, null);
+ }
+
+ public SubtitleView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
+
+ CharSequence text = "";
+ int textSize = 15;
+
+ final int n = a.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ switch (attr) {
+ case android.R.styleable.TextView_text:
+ text = a.getText(attr);
+ break;
+ case android.R.styleable.TextView_lineSpacingExtra:
+ mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
+ break;
+ case android.R.styleable.TextView_lineSpacingMultiplier:
+ mSpacingMult = a.getFloat(attr, mSpacingMult);
+ break;
+ case android.R.styleable.TextAppearance_textSize:
+ textSize = a.getDimensionPixelSize(attr, textSize);
+ break;
+ }
+ }
+
+ // Set up density-dependent properties.
+ // TODO: Move these to a default style.
+ final Resources res = getContext().getResources();
+ mCornerRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_corner_radius);
+ mOutlineWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_outline_width);
+ mShadowRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_radius);
+ mShadowOffsetX = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_offset);
+ mShadowOffsetY = mShadowOffsetX;
+
+ mTextPaint = new TextPaint();
+ mTextPaint.setAntiAlias(true);
+ mTextPaint.setSubpixelText(true);
+
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+
+ setText(text);
+ setTextSize(textSize);
+ }
+
+ public void setText(int resId) {
+ final CharSequence text = getContext().getText(resId);
+ setText(text);
+ }
+
+ public void setText(CharSequence text) {
+ mText.clear();
+ mText.append(text);
+
+ mHasMeasurements = false;
+
+ requestLayout();
+ invalidate();
+ }
+
+ public void setForegroundColor(int color) {
+ mForegroundColor = color;
+
+ invalidate();
+ }
+
+ @Override
+ public void setBackgroundColor(int color) {
+ mBackgroundColor = color;
+
+ invalidate();
+ }
+
+ public void setEdgeType(int edgeType) {
+ mEdgeType = edgeType;
+
+ invalidate();
+ }
+
+ public void setEdgeColor(int color) {
+ mEdgeColor = color;
+
+ invalidate();
+ }
+
+ /**
+ * Sets the text size in pixels.
+ *
+ * @param size the text size in pixels
+ */
+ public void setTextSize(float size) {
+ if (mTextPaint.getTextSize() != size) {
+ mTextPaint.setTextSize(size);
+ mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
+
+ mHasMeasurements = false;
+
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ public void setTypeface(Typeface typeface) {
+ if (mTextPaint.getTypeface() != typeface) {
+ mTextPaint.setTypeface(typeface);
+
+ mHasMeasurements = false;
+
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ public void setAlignment(Alignment textAlignment) {
+ if (mAlignment != textAlignment) {
+ mAlignment = textAlignment;
+
+ mHasMeasurements = false;
+
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
+
+ if (computeMeasurements(widthSpec)) {
+ final StaticLayout layout = mLayout;
+
+ // Account for padding.
+ final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
+ final int width = layout.getWidth() + paddingX;
+ final int height = layout.getHeight() + mPaddingTop + mPaddingBottom;
+ setMeasuredDimension(width, height);
+ } else {
+ setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
+ }
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int width = r - l;
+
+ computeMeasurements(width);
+ }
+
+ private boolean computeMeasurements(int maxWidth) {
+ if (mHasMeasurements && maxWidth == mLastMeasuredWidth) {
+ return true;
+ }
+
+ // Account for padding.
+ final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
+ maxWidth -= paddingX;
+ if (maxWidth <= 0) {
+ return false;
+ }
+
+ // TODO: Implement minimum-difference line wrapping. Adding the results
+ // of Paint.getTextWidths() seems to return different values than
+ // StaticLayout.getWidth(), so this is non-trivial.
+ mHasMeasurements = true;
+ mLastMeasuredWidth = maxWidth;
+ mLayout = StaticLayout.Builder.obtain(mText, 0, mText.length(), mTextPaint, maxWidth)
+ .setAlignment(mAlignment)
+ .setLineSpacing(mSpacingAdd, mSpacingMult)
+ .setUseLineSpacingFromFallbacks(true)
+ .build();
+
+ return true;
+ }
+
+ public void setStyle(int styleId) {
+ final Context context = mContext;
+ final ContentResolver cr = context.getContentResolver();
+ final CaptionStyle style;
+ if (styleId == CaptionStyle.PRESET_CUSTOM) {
+ style = CaptionStyle.getCustomStyle(cr);
+ } else {
+ style = CaptionStyle.PRESETS[styleId];
+ }
+
+ final CaptionStyle defStyle = CaptionStyle.DEFAULT;
+ mForegroundColor = style.hasForegroundColor() ?
+ style.foregroundColor : defStyle.foregroundColor;
+ mBackgroundColor = style.hasBackgroundColor() ?
+ style.backgroundColor : defStyle.backgroundColor;
+ mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType;
+ mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor;
+ mHasMeasurements = false;
+
+ final Typeface typeface = style.getTypeface();
+ setTypeface(typeface);
+
+ requestLayout();
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ final StaticLayout layout = mLayout;
+ if (layout == null) {
+ return;
+ }
+
+ final int saveCount = c.save();
+ final int innerPaddingX = mInnerPaddingX;
+ c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
+
+ final int lineCount = layout.getLineCount();
+ final Paint textPaint = mTextPaint;
+ final Paint paint = mPaint;
+ final RectF bounds = mLineBounds;
+
+ if (Color.alpha(mBackgroundColor) > 0) {
+ final float cornerRadius = mCornerRadius;
+ float previousBottom = layout.getLineTop(0);
+
+ paint.setColor(mBackgroundColor);
+ paint.setStyle(Style.FILL);
+
+ for (int i = 0; i < lineCount; i++) {
+ bounds.left = layout.getLineLeft(i) -innerPaddingX;
+ bounds.right = layout.getLineRight(i) + innerPaddingX;
+ bounds.top = previousBottom;
+ bounds.bottom = layout.getLineBottom(i);
+ previousBottom = bounds.bottom;
+
+ c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
+ }
+ }
+
+ final int edgeType = mEdgeType;
+ if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
+ textPaint.setStrokeJoin(Join.ROUND);
+ textPaint.setStrokeWidth(mOutlineWidth);
+ textPaint.setColor(mEdgeColor);
+ textPaint.setStyle(Style.FILL_AND_STROKE);
+
+ for (int i = 0; i < lineCount; i++) {
+ layout.drawText(c, i, i);
+ }
+ } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+ textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
+ } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED
+ || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) {
+ final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED;
+ final int colorUp = raised ? Color.WHITE : mEdgeColor;
+ final int colorDown = raised ? mEdgeColor : Color.WHITE;
+ final float offset = mShadowRadius / 2f;
+
+ textPaint.setColor(mForegroundColor);
+ textPaint.setStyle(Style.FILL);
+ textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
+
+ for (int i = 0; i < lineCount; i++) {
+ layout.drawText(c, i, i);
+ }
+
+ textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
+ }
+
+ textPaint.setColor(mForegroundColor);
+ textPaint.setStyle(Style.FILL);
+
+ for (int i = 0; i < lineCount; i++) {
+ layout.drawText(c, i, i);
+ }
+
+ textPaint.setShadowLayer(0, 0, 0, 0);
+ c.restoreToCount(saveCount);
+ }
+}
diff --git a/com/android/internal/widget/TextProgressBar.java b/com/android/internal/widget/TextProgressBar.java
new file mode 100644
index 0000000..7ca07d4
--- /dev/null
+++ b/com/android/internal/widget/TextProgressBar.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2008 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.internal.widget;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Chronometer;
+import android.widget.Chronometer.OnChronometerTickListener;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * Container that links together a {@link ProgressBar} and {@link Chronometer}
+ * as children. It subscribes to {@link Chronometer#OnChronometerTickListener}
+ * and updates the {@link ProgressBar} based on a preset finishing time.
+ * <p>
+ * This widget expects to contain two children with specific ids
+ * {@link android.R.id.progress} and {@link android.R.id.text1}.
+ * <p>
+ * If the {@link Chronometer} {@link android.R.attr#layout_width} is
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, then the
+ * {@link android.R.attr#gravity} will be used to automatically move it with
+ * respect to the {@link ProgressBar} position. For example, if
+ * {@link android.view.Gravity#LEFT} then the {@link Chronometer} will be placed
+ * just ahead of the leading edge of the {@link ProgressBar} position.
+ */
+@RemoteView
+public class TextProgressBar extends RelativeLayout implements OnChronometerTickListener {
+ public static final String TAG = "TextProgressBar";
+
+ static final int CHRONOMETER_ID = android.R.id.text1;
+ static final int PROGRESSBAR_ID = android.R.id.progress;
+
+ Chronometer mChronometer = null;
+ ProgressBar mProgressBar = null;
+
+ long mDurationBase = -1;
+ int mDuration = -1;
+
+ boolean mChronometerFollow = false;
+ int mChronometerGravity = Gravity.NO_GRAVITY;
+
+ public TextProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public TextProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public TextProgressBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TextProgressBar(Context context) {
+ super(context);
+ }
+
+ /**
+ * Catch any interesting children when they are added.
+ */
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+
+ int childId = child.getId();
+ if (childId == CHRONOMETER_ID && child instanceof Chronometer) {
+ mChronometer = (Chronometer) child;
+ mChronometer.setOnChronometerTickListener(this);
+
+ // Check if Chronometer should move with with ProgressBar
+ mChronometerFollow = (params.width == ViewGroup.LayoutParams.WRAP_CONTENT);
+ mChronometerGravity = (mChronometer.getGravity() &
+ Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK);
+
+ } else if (childId == PROGRESSBAR_ID && child instanceof ProgressBar) {
+ mProgressBar = (ProgressBar) child;
+ }
+ }
+
+ /**
+ * Set the expected termination time of the running {@link Chronometer}.
+ * This value is used to adjust the {@link ProgressBar} against the elapsed
+ * time.
+ * <p>
+ * Call this <b>after</b> adjusting the {@link Chronometer} base, if
+ * necessary.
+ *
+ * @param durationBase Use the {@link SystemClock#elapsedRealtime} time
+ * base.
+ */
+ @android.view.RemotableViewMethod
+ public void setDurationBase(long durationBase) {
+ mDurationBase = durationBase;
+
+ if (mProgressBar == null || mChronometer == null) {
+ throw new RuntimeException("Expecting child ProgressBar with id " +
+ "'android.R.id.progress' and Chronometer id 'android.R.id.text1'");
+ }
+
+ // Update the ProgressBar maximum relative to Chronometer base
+ mDuration = (int) (durationBase - mChronometer.getBase());
+ if (mDuration <= 0) {
+ mDuration = 1;
+ }
+ mProgressBar.setMax(mDuration);
+ }
+
+ /**
+ * Callback when {@link Chronometer} changes, indicating that we should
+ * update the {@link ProgressBar} and change the layout if necessary.
+ */
+ public void onChronometerTick(Chronometer chronometer) {
+ if (mProgressBar == null) {
+ throw new RuntimeException(
+ "Expecting child ProgressBar with id 'android.R.id.progress'");
+ }
+
+ // Stop Chronometer if we're past duration
+ long now = SystemClock.elapsedRealtime();
+ if (now >= mDurationBase) {
+ mChronometer.stop();
+ }
+
+ // Update the ProgressBar status
+ int remaining = (int) (mDurationBase - now);
+ mProgressBar.setProgress(mDuration - remaining);
+
+ // Move the Chronometer if gravity is set correctly
+ if (mChronometerFollow) {
+ RelativeLayout.LayoutParams params;
+
+ // Calculate estimate of ProgressBar leading edge position
+ params = (RelativeLayout.LayoutParams) mProgressBar.getLayoutParams();
+ int contentWidth = mProgressBar.getWidth() - (params.leftMargin + params.rightMargin);
+ int leadingEdge = ((contentWidth * mProgressBar.getProgress()) /
+ mProgressBar.getMax()) + params.leftMargin;
+
+ // Calculate any adjustment based on gravity
+ int adjustLeft = 0;
+ int textWidth = mChronometer.getWidth();
+ if (mChronometerGravity == Gravity.END) {
+ adjustLeft = -textWidth;
+ } else if (mChronometerGravity == Gravity.CENTER_HORIZONTAL) {
+ adjustLeft = -(textWidth / 2);
+ }
+
+ // Limit margin to keep text inside ProgressBar bounds
+ leadingEdge += adjustLeft;
+ int rightLimit = contentWidth - params.rightMargin - textWidth;
+ if (leadingEdge < params.leftMargin) {
+ leadingEdge = params.leftMargin;
+ } else if (leadingEdge > rightLimit) {
+ leadingEdge = rightLimit;
+ }
+
+ params = (RelativeLayout.LayoutParams) mChronometer.getLayoutParams();
+ params.leftMargin = leadingEdge;
+
+ // Request layout to move Chronometer
+ mChronometer.requestLayout();
+
+ }
+ }
+}
diff --git a/com/android/internal/widget/TextViewInputDisabler.java b/com/android/internal/widget/TextViewInputDisabler.java
new file mode 100644
index 0000000..57806eb
--- /dev/null
+++ b/com/android/internal/widget/TextViewInputDisabler.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.widget.TextView;
+
+/**
+ * Helper class to disable input on a TextView. The input is disabled by swapping in an InputFilter
+ * that discards all changes. Use with care if you have customized InputFilter on the target
+ * TextView.
+ */
+public class TextViewInputDisabler {
+ private TextView mTextView;
+ private InputFilter[] mDefaultFilters;
+ private InputFilter[] mNoInputFilters = new InputFilter[] {
+ new InputFilter () {
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
+ int dstart, int dend) {
+ return "";
+ }
+ }
+ };
+
+ @UnsupportedAppUsage
+ public TextViewInputDisabler(TextView textView) {
+ mTextView = textView;
+ mDefaultFilters = mTextView.getFilters();
+ }
+
+ @UnsupportedAppUsage
+ public void setInputEnabled(boolean enabled) {
+ mTextView.setFilters(enabled ? mDefaultFilters : mNoInputFilters);
+ }
+}
diff --git a/com/android/internal/widget/ToolbarWidgetWrapper.java b/com/android/internal/widget/ToolbarWidgetWrapper.java
new file mode 100644
index 0000000..32aae72
--- /dev/null
+++ b/com/android/internal/widget/ToolbarWidgetWrapper.java
@@ -0,0 +1,708 @@
+/*
+ * 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.internal.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.ActionMenuPresenter;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+import android.widget.Toolbar;
+import com.android.internal.R;
+import com.android.internal.view.menu.ActionMenuItem;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPresenter;
+
+/**
+ * Internal class used to interact with the Toolbar widget without
+ * exposing interface methods to the public API.
+ *
+ * <p>ToolbarWidgetWrapper manages the differences between Toolbar and ActionBarView
+ * so that either variant acting as a
+ * {@link com.android.internal.app.WindowDecorActionBar WindowDecorActionBar} can behave
+ * in the same way.</p>
+ *
+ * @hide
+ */
+public class ToolbarWidgetWrapper implements DecorToolbar {
+ private static final String TAG = "ToolbarWidgetWrapper";
+
+ private static final int AFFECTS_LOGO_MASK =
+ ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_USE_LOGO;
+ // Default fade duration for fading in/out tool bar.
+ private static final long DEFAULT_FADE_DURATION_MS = 200;
+
+ private Toolbar mToolbar;
+
+ private int mDisplayOpts;
+ private View mTabView;
+ private Spinner mSpinner;
+ private View mCustomView;
+
+ private Drawable mIcon;
+ private Drawable mLogo;
+ private Drawable mNavIcon;
+
+ private boolean mTitleSet;
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+ private CharSequence mHomeDescription;
+
+ private Window.Callback mWindowCallback;
+ private boolean mMenuPrepared;
+ private ActionMenuPresenter mActionMenuPresenter;
+
+ private int mNavigationMode = ActionBar.NAVIGATION_MODE_STANDARD;
+ private int mDefaultNavigationContentDescription = 0;
+ private Drawable mDefaultNavigationIcon;
+
+ public ToolbarWidgetWrapper(Toolbar toolbar, boolean style) {
+ this(toolbar, style, R.string.action_bar_up_description);
+ }
+
+ public ToolbarWidgetWrapper(Toolbar toolbar, boolean style,
+ int defaultNavigationContentDescription) {
+ mToolbar = toolbar;
+
+ mTitle = toolbar.getTitle();
+ mSubtitle = toolbar.getSubtitle();
+ mTitleSet = mTitle != null;
+ mNavIcon = mToolbar.getNavigationIcon();
+ final TypedArray a = toolbar.getContext().obtainStyledAttributes(null,
+ R.styleable.ActionBar, R.attr.actionBarStyle, 0);
+ mDefaultNavigationIcon = a.getDrawable(R.styleable.ActionBar_homeAsUpIndicator);
+ if (style) {
+ final CharSequence title = a.getText(R.styleable.ActionBar_title);
+ if (!TextUtils.isEmpty(title)) {
+ setTitle(title);
+ }
+
+ final CharSequence subtitle = a.getText(R.styleable.ActionBar_subtitle);
+ if (!TextUtils.isEmpty(subtitle)) {
+ setSubtitle(subtitle);
+ }
+
+ final Drawable logo = a.getDrawable(R.styleable.ActionBar_logo);
+ if (logo != null) {
+ setLogo(logo);
+ }
+
+ final Drawable icon = a.getDrawable(R.styleable.ActionBar_icon);
+ if (icon != null) {
+ setIcon(icon);
+ }
+ if (mNavIcon == null && mDefaultNavigationIcon != null) {
+ setNavigationIcon(mDefaultNavigationIcon);
+ }
+ setDisplayOptions(a.getInt(R.styleable.ActionBar_displayOptions, 0));
+
+ final int customNavId = a.getResourceId(
+ R.styleable.ActionBar_customNavigationLayout, 0);
+ if (customNavId != 0) {
+ setCustomView(LayoutInflater.from(mToolbar.getContext()).inflate(customNavId,
+ mToolbar, false));
+ setDisplayOptions(mDisplayOpts | ActionBar.DISPLAY_SHOW_CUSTOM);
+ }
+
+ final int height = a.getLayoutDimension(R.styleable.ActionBar_height, 0);
+ if (height > 0) {
+ final ViewGroup.LayoutParams lp = mToolbar.getLayoutParams();
+ lp.height = height;
+ mToolbar.setLayoutParams(lp);
+ }
+
+ final int contentInsetStart = a.getDimensionPixelOffset(
+ R.styleable.ActionBar_contentInsetStart, -1);
+ final int contentInsetEnd = a.getDimensionPixelOffset(
+ R.styleable.ActionBar_contentInsetEnd, -1);
+ if (contentInsetStart >= 0 || contentInsetEnd >= 0) {
+ mToolbar.setContentInsetsRelative(Math.max(contentInsetStart, 0),
+ Math.max(contentInsetEnd, 0));
+ }
+
+ final int titleTextStyle = a.getResourceId(R.styleable.ActionBar_titleTextStyle, 0);
+ if (titleTextStyle != 0) {
+ mToolbar.setTitleTextAppearance(mToolbar.getContext(), titleTextStyle);
+ }
+
+ final int subtitleTextStyle = a.getResourceId(
+ R.styleable.ActionBar_subtitleTextStyle, 0);
+ if (subtitleTextStyle != 0) {
+ mToolbar.setSubtitleTextAppearance(mToolbar.getContext(), subtitleTextStyle);
+ }
+
+ final int popupTheme = a.getResourceId(R.styleable.ActionBar_popupTheme, 0);
+ if (popupTheme != 0) {
+ mToolbar.setPopupTheme(popupTheme);
+ }
+ } else {
+ mDisplayOpts = detectDisplayOptions();
+ }
+ a.recycle();
+
+ setDefaultNavigationContentDescription(defaultNavigationContentDescription);
+ mHomeDescription = mToolbar.getNavigationContentDescription();
+
+ mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ final ActionMenuItem mNavItem = new ActionMenuItem(mToolbar.getContext(),
+ 0, android.R.id.home, 0, 0, mTitle);
+ @Override
+ public void onClick(View v) {
+ if (mWindowCallback != null && mMenuPrepared) {
+ mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, mNavItem);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setDefaultNavigationContentDescription(int defaultNavigationContentDescription) {
+ if (defaultNavigationContentDescription == mDefaultNavigationContentDescription) {
+ return;
+ }
+ mDefaultNavigationContentDescription = defaultNavigationContentDescription;
+ if (TextUtils.isEmpty(mToolbar.getNavigationContentDescription())) {
+ setNavigationContentDescription(mDefaultNavigationContentDescription);
+ }
+ }
+
+ private int detectDisplayOptions() {
+ int opts = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME |
+ ActionBar.DISPLAY_USE_LOGO;
+ if (mToolbar.getNavigationIcon() != null) {
+ opts |= ActionBar.DISPLAY_HOME_AS_UP;
+ mDefaultNavigationIcon = mToolbar.getNavigationIcon();
+ }
+ return opts;
+ }
+
+ @Override
+ public ViewGroup getViewGroup() {
+ return mToolbar;
+ }
+
+ @Override
+ public Context getContext() {
+ return mToolbar.getContext();
+ }
+
+ @Override
+ public boolean isSplit() {
+ return false;
+ }
+
+ @Override
+ public boolean hasExpandedActionView() {
+ return mToolbar.hasExpandedActionView();
+ }
+
+ @Override
+ public void collapseActionView() {
+ mToolbar.collapseActionView();
+ }
+
+ @Override
+ public void setWindowCallback(Window.Callback cb) {
+ mWindowCallback = cb;
+ }
+
+ @Override
+ public void setWindowTitle(CharSequence title) {
+ // "Real" title always trumps window title.
+ if (!mTitleSet) {
+ setTitleInt(title);
+ }
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return mToolbar.getTitle();
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ mTitleSet = true;
+ setTitleInt(title);
+ }
+
+ private void setTitleInt(CharSequence title) {
+ mTitle = title;
+ if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ mToolbar.setTitle(title);
+ }
+ }
+
+ @Override
+ public CharSequence getSubtitle() {
+ return mToolbar.getSubtitle();
+ }
+
+ @Override
+ public void setSubtitle(CharSequence subtitle) {
+ mSubtitle = subtitle;
+ if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ mToolbar.setSubtitle(subtitle);
+ }
+ }
+
+ @Override
+ public void initProgress() {
+ Log.i(TAG, "Progress display unsupported");
+ }
+
+ @Override
+ public void initIndeterminateProgress() {
+ Log.i(TAG, "Progress display unsupported");
+ }
+
+ @Override
+ public boolean canSplit() {
+ return false;
+ }
+
+ @Override
+ public void setSplitView(ViewGroup splitView) {
+ }
+
+ @Override
+ public void setSplitToolbar(boolean split) {
+ if (split) {
+ throw new UnsupportedOperationException("Cannot split an android.widget.Toolbar");
+ }
+ }
+
+ @Override
+ public void setSplitWhenNarrow(boolean splitWhenNarrow) {
+ // Ignore.
+ }
+
+ @Override
+ public boolean hasIcon() {
+ return mIcon != null;
+ }
+
+ @Override
+ public boolean hasLogo() {
+ return mLogo != null;
+ }
+
+ @Override
+ public void setIcon(int resId) {
+ setIcon(resId != 0 ? getContext().getDrawable(resId) : null);
+ }
+
+ @Override
+ public void setIcon(Drawable d) {
+ mIcon = d;
+ updateToolbarLogo();
+ }
+
+ @Override
+ public void setLogo(int resId) {
+ setLogo(resId != 0 ? getContext().getDrawable(resId) : null);
+ }
+
+ @Override
+ public void setLogo(Drawable d) {
+ mLogo = d;
+ updateToolbarLogo();
+ }
+
+ private void updateToolbarLogo() {
+ Drawable logo = null;
+ if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_HOME) != 0) {
+ if ((mDisplayOpts & ActionBar.DISPLAY_USE_LOGO) != 0) {
+ logo = mLogo != null ? mLogo : mIcon;
+ } else {
+ logo = mIcon;
+ }
+ }
+ mToolbar.setLogo(logo);
+ }
+
+ @Override
+ public boolean canShowOverflowMenu() {
+ return mToolbar.canShowOverflowMenu();
+ }
+
+ @Override
+ public boolean isOverflowMenuShowing() {
+ return mToolbar.isOverflowMenuShowing();
+ }
+
+ @Override
+ public boolean isOverflowMenuShowPending() {
+ return mToolbar.isOverflowMenuShowPending();
+ }
+
+ @Override
+ public boolean showOverflowMenu() {
+ return mToolbar.showOverflowMenu();
+ }
+
+ @Override
+ public boolean hideOverflowMenu() {
+ return mToolbar.hideOverflowMenu();
+ }
+
+ @Override
+ public void setMenuPrepared() {
+ mMenuPrepared = true;
+ }
+
+ @Override
+ public void setMenu(Menu menu, MenuPresenter.Callback cb) {
+ if (mActionMenuPresenter == null) {
+ mActionMenuPresenter = new ActionMenuPresenter(mToolbar.getContext());
+ mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter);
+ }
+ mActionMenuPresenter.setCallback(cb);
+ mToolbar.setMenu((MenuBuilder) menu, mActionMenuPresenter);
+ }
+
+ @Override
+ public void dismissPopupMenus() {
+ mToolbar.dismissPopupMenus();
+ }
+
+ @Override
+ public int getDisplayOptions() {
+ return mDisplayOpts;
+ }
+
+ @Override
+ public void setDisplayOptions(int newOpts) {
+ final int oldOpts = mDisplayOpts;
+ final int changed = oldOpts ^ newOpts;
+ mDisplayOpts = newOpts;
+ if (changed != 0) {
+ if ((changed & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+ if ((newOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+ updateHomeAccessibility();
+ }
+ updateNavigationIcon();
+ }
+
+ if ((changed & AFFECTS_LOGO_MASK) != 0) {
+ updateToolbarLogo();
+ }
+
+ if ((changed & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ if ((newOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) {
+ mToolbar.setTitle(mTitle);
+ mToolbar.setSubtitle(mSubtitle);
+ } else {
+ mToolbar.setTitle(null);
+ mToolbar.setSubtitle(null);
+ }
+ }
+
+ if ((changed & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && mCustomView != null) {
+ if ((newOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+ mToolbar.addView(mCustomView);
+ } else {
+ mToolbar.removeView(mCustomView);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setEmbeddedTabView(ScrollingTabContainerView tabView) {
+ if (mTabView != null && mTabView.getParent() == mToolbar) {
+ mToolbar.removeView(mTabView);
+ }
+ mTabView = tabView;
+ if (tabView != null && mNavigationMode == ActionBar.NAVIGATION_MODE_TABS) {
+ mToolbar.addView(mTabView, 0);
+ Toolbar.LayoutParams lp = (Toolbar.LayoutParams) mTabView.getLayoutParams();
+ lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+ lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ lp.gravity = Gravity.START | Gravity.BOTTOM;
+ tabView.setAllowCollapse(true);
+ }
+ }
+
+ @Override
+ public boolean hasEmbeddedTabs() {
+ return mTabView != null;
+ }
+
+ @Override
+ public boolean isTitleTruncated() {
+ return mToolbar.isTitleTruncated();
+ }
+
+ @Override
+ public void setCollapsible(boolean collapsible) {
+ mToolbar.setCollapsible(collapsible);
+ }
+
+ @Override
+ public void setHomeButtonEnabled(boolean enable) {
+ // Ignore
+ }
+
+ @Override
+ public int getNavigationMode() {
+ return mNavigationMode;
+ }
+
+ @Override
+ public void setNavigationMode(int mode) {
+ final int oldMode = mNavigationMode;
+ if (mode != oldMode) {
+ switch (oldMode) {
+ case ActionBar.NAVIGATION_MODE_LIST:
+ if (mSpinner != null && mSpinner.getParent() == mToolbar) {
+ mToolbar.removeView(mSpinner);
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabView != null && mTabView.getParent() == mToolbar) {
+ mToolbar.removeView(mTabView);
+ }
+ break;
+ }
+
+ mNavigationMode = mode;
+
+ switch (mode) {
+ case ActionBar.NAVIGATION_MODE_STANDARD:
+ break;
+ case ActionBar.NAVIGATION_MODE_LIST:
+ ensureSpinner();
+ mToolbar.addView(mSpinner, 0);
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabView != null) {
+ mToolbar.addView(mTabView, 0);
+ Toolbar.LayoutParams lp = (Toolbar.LayoutParams) mTabView.getLayoutParams();
+ lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+ lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ lp.gravity = Gravity.START | Gravity.BOTTOM;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid navigation mode " + mode);
+ }
+ }
+ }
+
+ private void ensureSpinner() {
+ if (mSpinner == null) {
+ mSpinner = new Spinner(getContext(), null, R.attr.actionDropDownStyle);
+ Toolbar.LayoutParams lp = new Toolbar.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.CENTER_VERTICAL);
+ mSpinner.setLayoutParams(lp);
+ }
+ }
+
+ @Override
+ public void setDropdownParams(SpinnerAdapter adapter,
+ AdapterView.OnItemSelectedListener listener) {
+ ensureSpinner();
+ mSpinner.setAdapter(adapter);
+ mSpinner.setOnItemSelectedListener(listener);
+ }
+
+ @Override
+ public void setDropdownSelectedPosition(int position) {
+ if (mSpinner == null) {
+ throw new IllegalStateException(
+ "Can't set dropdown selected position without an adapter");
+ }
+ mSpinner.setSelection(position);
+ }
+
+ @Override
+ public int getDropdownSelectedPosition() {
+ return mSpinner != null ? mSpinner.getSelectedItemPosition() : 0;
+ }
+
+ @Override
+ public int getDropdownItemCount() {
+ return mSpinner != null ? mSpinner.getCount() : 0;
+ }
+
+ @Override
+ public void setCustomView(View view) {
+ if (mCustomView != null && (mDisplayOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+ mToolbar.removeView(mCustomView);
+ }
+ mCustomView = view;
+ if (view != null && (mDisplayOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) {
+ mToolbar.addView(mCustomView);
+ }
+ }
+
+ @Override
+ public View getCustomView() {
+ return mCustomView;
+ }
+
+ @Override
+ public void animateToVisibility(int visibility) {
+ Animator anim = setupAnimatorToVisibility(visibility, DEFAULT_FADE_DURATION_MS);
+ if (anim != null) {
+ anim.start();
+ }
+ }
+
+ @Override
+ public Animator setupAnimatorToVisibility(int visibility, long duration) {
+
+ if (visibility == View.GONE) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(mToolbar, View.ALPHA, 1, 0);
+ anim.setDuration(duration);
+ anim.addListener(new AnimatorListenerAdapter() {
+ private boolean mCanceled = false;
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCanceled) {
+ mToolbar.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCanceled = true;
+ }
+ });
+ return anim;
+ } else if (visibility == View.VISIBLE) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(mToolbar, View.ALPHA, 0, 1);
+ anim.setDuration(duration);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mToolbar.setVisibility(View.VISIBLE);
+ }
+ });
+ return anim;
+ }
+ return null;
+ }
+
+ @Override
+ public void setNavigationIcon(Drawable icon) {
+ mNavIcon = icon;
+ updateNavigationIcon();
+ }
+
+ @Override
+ public void setNavigationIcon(int resId) {
+ setNavigationIcon(resId != 0 ? mToolbar.getContext().getDrawable(resId) : null);
+ }
+
+ @Override
+ public void setDefaultNavigationIcon(Drawable defaultNavigationIcon) {
+ if (mDefaultNavigationIcon != defaultNavigationIcon) {
+ mDefaultNavigationIcon = defaultNavigationIcon;
+ updateNavigationIcon();
+ }
+ }
+
+ private void updateNavigationIcon() {
+ if ((mDisplayOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+ mToolbar.setNavigationIcon(mNavIcon != null ? mNavIcon : mDefaultNavigationIcon);
+ } else {
+ mToolbar.setNavigationIcon(null);
+ }
+ }
+
+ @Override
+ public void setNavigationContentDescription(CharSequence description) {
+ mHomeDescription = description;
+ updateHomeAccessibility();
+ }
+
+ @Override
+ public void setNavigationContentDescription(int resId) {
+ setNavigationContentDescription(resId == 0 ? null : getContext().getString(resId));
+ }
+
+ private void updateHomeAccessibility() {
+ if ((mDisplayOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+ if (TextUtils.isEmpty(mHomeDescription)) {
+ mToolbar.setNavigationContentDescription(mDefaultNavigationContentDescription);
+ } else {
+ mToolbar.setNavigationContentDescription(mHomeDescription);
+ }
+ }
+ }
+
+ @Override
+ public void saveHierarchyState(SparseArray<Parcelable> toolbarStates) {
+ mToolbar.saveHierarchyState(toolbarStates);
+ }
+
+ @Override
+ public void restoreHierarchyState(SparseArray<Parcelable> toolbarStates) {
+ mToolbar.restoreHierarchyState(toolbarStates);
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable d) {
+ //noinspection deprecation
+ mToolbar.setBackgroundDrawable(d);
+ }
+
+ @Override
+ public int getHeight() {
+ return mToolbar.getHeight();
+ }
+
+ @Override
+ public void setVisibility(int visible) {
+ mToolbar.setVisibility(visible);
+ }
+
+ @Override
+ public int getVisibility() {
+ return mToolbar.getVisibility();
+ }
+
+ @Override
+ public void setMenuCallbacks(MenuPresenter.Callback presenterCallback,
+ MenuBuilder.Callback menuBuilderCallback) {
+ mToolbar.setMenuCallbacks(presenterCallback, menuBuilderCallback);
+ }
+
+ @Override
+ public Menu getMenu() {
+ return mToolbar.getMenu();
+ }
+
+}
diff --git a/com/android/internal/widget/VerifyCredentialResponse.java b/com/android/internal/widget/VerifyCredentialResponse.java
new file mode 100644
index 0000000..7d1c706
--- /dev/null
+++ b/com/android/internal/widget/VerifyCredentialResponse.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.gatekeeper.GateKeeperResponse;
+import android.util.Slog;
+
+/**
+ * Response object for a ILockSettings credential verification request.
+ * @hide
+ */
+public final class VerifyCredentialResponse implements Parcelable {
+
+ public static final int RESPONSE_ERROR = -1;
+ public static final int RESPONSE_OK = 0;
+ public static final int RESPONSE_RETRY = 1;
+
+ public static final VerifyCredentialResponse OK = new VerifyCredentialResponse();
+ public static final VerifyCredentialResponse ERROR
+ = new VerifyCredentialResponse(RESPONSE_ERROR, 0, null);
+ private static final String TAG = "VerifyCredentialResponse";
+
+ private int mResponseCode;
+ private byte[] mPayload;
+ private int mTimeout;
+
+ public static final Parcelable.Creator<VerifyCredentialResponse> CREATOR
+ = new Parcelable.Creator<VerifyCredentialResponse>() {
+ @Override
+ public VerifyCredentialResponse createFromParcel(Parcel source) {
+ int responseCode = source.readInt();
+ VerifyCredentialResponse response = new VerifyCredentialResponse(responseCode, 0, null);
+ if (responseCode == RESPONSE_RETRY) {
+ response.setTimeout(source.readInt());
+ } else if (responseCode == RESPONSE_OK) {
+ int size = source.readInt();
+ if (size > 0) {
+ byte[] payload = new byte[size];
+ source.readByteArray(payload);
+ response.setPayload(payload);
+ }
+ }
+ return response;
+ }
+
+ @Override
+ public VerifyCredentialResponse[] newArray(int size) {
+ return new VerifyCredentialResponse[size];
+ }
+
+ };
+
+ public VerifyCredentialResponse() {
+ mResponseCode = RESPONSE_OK;
+ mPayload = null;
+ }
+
+
+ public VerifyCredentialResponse(byte[] payload) {
+ mPayload = payload;
+ mResponseCode = RESPONSE_OK;
+ }
+
+ public VerifyCredentialResponse(int timeout) {
+ mTimeout = timeout;
+ mResponseCode = RESPONSE_RETRY;
+ mPayload = null;
+ }
+
+ private VerifyCredentialResponse(int responseCode, int timeout, byte[] payload) {
+ mResponseCode = responseCode;
+ mTimeout = timeout;
+ mPayload = payload;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mResponseCode);
+ if (mResponseCode == RESPONSE_RETRY) {
+ dest.writeInt(mTimeout);
+ } else if (mResponseCode == RESPONSE_OK) {
+ if (mPayload != null) {
+ dest.writeInt(mPayload.length);
+ dest.writeByteArray(mPayload);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public byte[] getPayload() {
+ return mPayload;
+ }
+
+ public int getTimeout() {
+ return mTimeout;
+ }
+
+ public int getResponseCode() {
+ return mResponseCode;
+ }
+
+ private void setTimeout(int timeout) {
+ mTimeout = timeout;
+ }
+
+ private void setPayload(byte[] payload) {
+ mPayload = payload;
+ }
+
+ public VerifyCredentialResponse stripPayload() {
+ return new VerifyCredentialResponse(mResponseCode, mTimeout, new byte[0]);
+ }
+
+ public static VerifyCredentialResponse fromGateKeeperResponse(
+ GateKeeperResponse gateKeeperResponse) {
+ VerifyCredentialResponse response;
+ int responseCode = gateKeeperResponse.getResponseCode();
+ if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+ response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
+ } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+ byte[] token = gateKeeperResponse.getPayload();
+ if (token == null) {
+ // something's wrong if there's no payload with a challenge
+ Slog.e(TAG, "verifyChallenge response had no associated payload");
+ response = VerifyCredentialResponse.ERROR;
+ } else {
+ response = new VerifyCredentialResponse(token);
+ }
+ } else {
+ response = VerifyCredentialResponse.ERROR;
+ }
+ return response;
+ }
+}
diff --git a/com/android/internal/widget/ViewClippingUtil.java b/com/android/internal/widget/ViewClippingUtil.java
new file mode 100644
index 0000000..59bbed4
--- /dev/null
+++ b/com/android/internal/widget/ViewClippingUtil.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.util.ArraySet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.internal.R;
+
+/**
+ * A utility class that allows to clip views and their parents to allow for better transitions
+ */
+public class ViewClippingUtil {
+ private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag;
+ private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag;
+ private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag;
+
+ public static void setClippingDeactivated(final View transformedView, boolean deactivated,
+ ClippingParameters clippingParameters) {
+ if (!deactivated && !clippingParameters.isClippingEnablingAllowed(transformedView)) {
+ return;
+ }
+ if (!(transformedView.getParent() instanceof ViewGroup)) {
+ return;
+ }
+ ViewGroup parent = (ViewGroup) transformedView.getParent();
+ while (true) {
+ if (!deactivated && !clippingParameters.isClippingEnablingAllowed(transformedView)) {
+ return;
+ }
+ ArraySet<View> clipSet = (ArraySet<View>) parent.getTag(CLIP_CLIPPING_SET);
+ if (clipSet == null) {
+ clipSet = new ArraySet<>();
+ parent.setTagInternal(CLIP_CLIPPING_SET, clipSet);
+ }
+ Boolean clipChildren = (Boolean) parent.getTag(CLIP_CHILDREN_TAG);
+ if (clipChildren == null) {
+ clipChildren = parent.getClipChildren();
+ parent.setTagInternal(CLIP_CHILDREN_TAG, clipChildren);
+ }
+ Boolean clipToPadding = (Boolean) parent.getTag(CLIP_TO_PADDING);
+ if (clipToPadding == null) {
+ clipToPadding = parent.getClipToPadding();
+ parent.setTagInternal(CLIP_TO_PADDING, clipToPadding);
+ }
+ if (!deactivated) {
+ clipSet.remove(transformedView);
+ if (clipSet.isEmpty()) {
+ parent.setClipChildren(clipChildren);
+ parent.setClipToPadding(clipToPadding);
+ parent.setTagInternal(CLIP_CLIPPING_SET, null);
+ clippingParameters.onClippingStateChanged(parent, true);
+ }
+ } else {
+ clipSet.add(transformedView);
+ parent.setClipChildren(false);
+ parent.setClipToPadding(false);
+ clippingParameters.onClippingStateChanged(parent, false);
+ }
+ if (clippingParameters.shouldFinish(parent)) {
+ return;
+ }
+ final ViewParent viewParent = parent.getParent();
+ if (viewParent instanceof ViewGroup) {
+ parent = (ViewGroup) viewParent;
+ } else {
+ return;
+ }
+ }
+ }
+
+ public interface ClippingParameters {
+ /**
+ * Should we stop clipping at this view? If true is returned, {@param view} is the last view
+ * where clipping is activated / deactivated.
+ */
+ boolean shouldFinish(View view);
+
+ /**
+ * Is it allowed to enable clipping on this view.
+ */
+ default boolean isClippingEnablingAllowed(View view) {
+ return !MessagingPropertyAnimator.isAnimatingTranslation(view);
+ }
+
+ /**
+ * A method that is called whenever the view starts clipping again / stops clipping to the
+ * children and padding.
+ */
+ default void onClippingStateChanged(View view, boolean isClipping) {};
+ }
+}
diff --git a/com/android/internal/widget/ViewInfoStore.java b/com/android/internal/widget/ViewInfoStore.java
new file mode 100644
index 0000000..6784a85
--- /dev/null
+++ b/com/android/internal/widget/ViewInfoStore.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.LongSparseArray;
+import android.util.Pools;
+
+import static com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
+import static com.android.internal.widget.RecyclerView.ViewHolder;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_POST;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE;
+import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * This class abstracts all tracking for Views to run animations.
+ */
+class ViewInfoStore {
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * View data records for pre-layout
+ */
+ @VisibleForTesting
+ final ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();
+
+ @VisibleForTesting
+ final LongSparseArray<ViewHolder> mOldChangedHolders = new LongSparseArray<>();
+
+ /**
+ * Clears the state and all existing tracking data
+ */
+ void clear() {
+ mLayoutHolderMap.clear();
+ mOldChangedHolders.clear();
+ }
+
+ /**
+ * Adds the item information to the prelayout tracking
+ * @param holder The ViewHolder whose information is being saved
+ * @param info The information to save
+ */
+ void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
+ InfoRecord record = mLayoutHolderMap.get(holder);
+ if (record == null) {
+ record = InfoRecord.obtain();
+ mLayoutHolderMap.put(holder, record);
+ }
+ record.preInfo = info;
+ record.flags |= FLAG_PRE;
+ }
+
+ boolean isDisappearing(ViewHolder holder) {
+ final InfoRecord record = mLayoutHolderMap.get(holder);
+ return record != null && ((record.flags & FLAG_DISAPPEARED) != 0);
+ }
+
+ /**
+ * Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it.
+ *
+ * @param vh The ViewHolder whose information is being queried
+ * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
+ */
+ @Nullable
+ ItemHolderInfo popFromPreLayout(ViewHolder vh) {
+ return popFromLayoutStep(vh, FLAG_PRE);
+ }
+
+ /**
+ * Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it.
+ *
+ * @param vh The ViewHolder whose information is being queried
+ * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
+ */
+ @Nullable
+ ItemHolderInfo popFromPostLayout(ViewHolder vh) {
+ return popFromLayoutStep(vh, FLAG_POST);
+ }
+
+ private ItemHolderInfo popFromLayoutStep(ViewHolder vh, int flag) {
+ int index = mLayoutHolderMap.indexOfKey(vh);
+ if (index < 0) {
+ return null;
+ }
+ final InfoRecord record = mLayoutHolderMap.valueAt(index);
+ if (record != null && (record.flags & flag) != 0) {
+ record.flags &= ~flag;
+ final ItemHolderInfo info;
+ if (flag == FLAG_PRE) {
+ info = record.preInfo;
+ } else if (flag == FLAG_POST) {
+ info = record.postInfo;
+ } else {
+ throw new IllegalArgumentException("Must provide flag PRE or POST");
+ }
+ // if not pre-post flag is left, clear.
+ if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) {
+ mLayoutHolderMap.removeAt(index);
+ InfoRecord.recycle(record);
+ }
+ return info;
+ }
+ return null;
+ }
+
+ /**
+ * Adds the given ViewHolder to the oldChangeHolders list
+ * @param key The key to identify the ViewHolder.
+ * @param holder The ViewHolder to store
+ */
+ void addToOldChangeHolders(long key, ViewHolder holder) {
+ mOldChangedHolders.put(key, holder);
+ }
+
+ /**
+ * Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the
+ * LayoutManager during a pre-layout pass. We distinguish them from other views that were
+ * already in the pre-layout so that ItemAnimator can choose to run a different animation for
+ * them.
+ *
+ * @param holder The ViewHolder to store
+ * @param info The information to save
+ */
+ void addToAppearedInPreLayoutHolders(ViewHolder holder, ItemHolderInfo info) {
+ InfoRecord record = mLayoutHolderMap.get(holder);
+ if (record == null) {
+ record = InfoRecord.obtain();
+ mLayoutHolderMap.put(holder, record);
+ }
+ record.flags |= FLAG_APPEAR;
+ record.preInfo = info;
+ }
+
+ /**
+ * Checks whether the given ViewHolder is in preLayout list
+ * @param viewHolder The ViewHolder to query
+ *
+ * @return True if the ViewHolder is present in preLayout, false otherwise
+ */
+ boolean isInPreLayout(ViewHolder viewHolder) {
+ final InfoRecord record = mLayoutHolderMap.get(viewHolder);
+ return record != null && (record.flags & FLAG_PRE) != 0;
+ }
+
+ /**
+ * Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns
+ * null.
+ * @param key The key to be used to find the ViewHolder.
+ *
+ * @return A ViewHolder if exists or null if it does not exist.
+ */
+ ViewHolder getFromOldChangeHolders(long key) {
+ return mOldChangedHolders.get(key);
+ }
+
+ /**
+ * Adds the item information to the post layout list
+ * @param holder The ViewHolder whose information is being saved
+ * @param info The information to save
+ */
+ void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
+ InfoRecord record = mLayoutHolderMap.get(holder);
+ if (record == null) {
+ record = InfoRecord.obtain();
+ mLayoutHolderMap.put(holder, record);
+ }
+ record.postInfo = info;
+ record.flags |= FLAG_POST;
+ }
+
+ /**
+ * A ViewHolder might be added by the LayoutManager just to animate its disappearance.
+ * This list holds such items so that we can animate / recycle these ViewHolders properly.
+ *
+ * @param holder The ViewHolder which disappeared during a layout.
+ */
+ void addToDisappearedInLayout(ViewHolder holder) {
+ InfoRecord record = mLayoutHolderMap.get(holder);
+ if (record == null) {
+ record = InfoRecord.obtain();
+ mLayoutHolderMap.put(holder, record);
+ }
+ record.flags |= FLAG_DISAPPEARED;
+ }
+
+ /**
+ * Removes a ViewHolder from disappearing list.
+ * @param holder The ViewHolder to be removed from the disappearing list.
+ */
+ void removeFromDisappearedInLayout(ViewHolder holder) {
+ InfoRecord record = mLayoutHolderMap.get(holder);
+ if (record == null) {
+ return;
+ }
+ record.flags &= ~FLAG_DISAPPEARED;
+ }
+
+ void process(ProcessCallback callback) {
+ for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
+ final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
+ final InfoRecord record = mLayoutHolderMap.removeAt(index);
+ if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
+ // Appeared then disappeared. Not useful for animations.
+ callback.unused(viewHolder);
+ } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
+ // Set as "disappeared" by the LayoutManager (addDisappearingView)
+ if (record.preInfo == null) {
+ // similar to appear disappear but happened between different layout passes.
+ // this can happen when the layout manager is using auto-measure
+ callback.unused(viewHolder);
+ } else {
+ callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
+ }
+ } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
+ // Appeared in the layout but not in the adapter (e.g. entered the viewport)
+ callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
+ } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
+ // Persistent in both passes. Animate persistence
+ callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
+ } else if ((record.flags & FLAG_PRE) != 0) {
+ // Was in pre-layout, never been added to post layout
+ callback.processDisappeared(viewHolder, record.preInfo, null);
+ } else if ((record.flags & FLAG_POST) != 0) {
+ // Was not in pre-layout, been added to post layout
+ callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
+ } else if ((record.flags & FLAG_APPEAR) != 0) {
+ // Scrap view. RecyclerView will handle removing/recycling this.
+ } else if (DEBUG) {
+ throw new IllegalStateException("record without any reasonable flag combination:/");
+ }
+ InfoRecord.recycle(record);
+ }
+ }
+
+ /**
+ * Removes the ViewHolder from all list
+ * @param holder The ViewHolder which we should stop tracking
+ */
+ void removeViewHolder(ViewHolder holder) {
+ for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) {
+ if (holder == mOldChangedHolders.valueAt(i)) {
+ mOldChangedHolders.removeAt(i);
+ break;
+ }
+ }
+ final InfoRecord info = mLayoutHolderMap.remove(holder);
+ if (info != null) {
+ InfoRecord.recycle(info);
+ }
+ }
+
+ void onDetach() {
+ InfoRecord.drainCache();
+ }
+
+ public void onViewDetached(ViewHolder viewHolder) {
+ removeFromDisappearedInLayout(viewHolder);
+ }
+
+ interface ProcessCallback {
+ void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo,
+ @Nullable ItemHolderInfo postInfo);
+ void processAppeared(ViewHolder viewHolder, @Nullable ItemHolderInfo preInfo,
+ ItemHolderInfo postInfo);
+ void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo,
+ @NonNull ItemHolderInfo postInfo);
+ void unused(ViewHolder holder);
+ }
+
+ static class InfoRecord {
+ // disappearing list
+ static final int FLAG_DISAPPEARED = 1;
+ // appear in pre layout list
+ static final int FLAG_APPEAR = 1 << 1;
+ // pre layout, this is necessary to distinguish null item info
+ static final int FLAG_PRE = 1 << 2;
+ // post layout, this is necessary to distinguish null item info
+ static final int FLAG_POST = 1 << 3;
+ static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
+ static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
+ static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
+ int flags;
+ @Nullable ItemHolderInfo preInfo;
+ @Nullable ItemHolderInfo postInfo;
+ static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);
+
+ private InfoRecord() {
+ }
+
+ static InfoRecord obtain() {
+ InfoRecord record = sPool.acquire();
+ return record == null ? new InfoRecord() : record;
+ }
+
+ static void recycle(InfoRecord record) {
+ record.flags = 0;
+ record.preInfo = null;
+ record.postInfo = null;
+ sPool.release(record);
+ }
+
+ static void drainCache() {
+ //noinspection StatementWithEmptyBody
+ while (sPool.acquire() != null);
+ }
+ }
+}
diff --git a/com/android/internal/widget/ViewPager.java b/com/android/internal/widget/ViewPager.java
new file mode 100644
index 0000000..c8a86d1
--- /dev/null
+++ b/com/android/internal/widget/ViewPager.java
@@ -0,0 +1,2838 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.AbsSavedState;
+import android.view.FocusFinder;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.Interpolator;
+import android.view.inspector.InspectableProperty;
+import android.widget.EdgeEffect;
+import android.widget.Scroller;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Framework copy of the support-v4 ViewPager class.
+ */
+public class ViewPager extends ViewGroup {
+ private static final String TAG = "ViewPager";
+ private static final boolean DEBUG = false;
+
+ private static final int MAX_SCROLL_X = 2 << 23;
+ private static final boolean USE_CACHE = false;
+
+ private static final int DEFAULT_OFFSCREEN_PAGES = 1;
+ private static final int MAX_SETTLE_DURATION = 600; // ms
+ private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
+
+ private static final int DEFAULT_GUTTER_SIZE = 16; // dips
+
+ private static final int MIN_FLING_VELOCITY = 400; // dips
+
+ private static final int[] LAYOUT_ATTRS = new int[] {
+ com.android.internal.R.attr.layout_gravity
+ };
+
+ /**
+ * Used to track what the expected number of items in the adapter should be.
+ * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
+ */
+ private int mExpectedAdapterCount;
+
+ static class ItemInfo {
+ Object object;
+ boolean scrolling;
+ float widthFactor;
+
+ /** Logical position of the item within the pager adapter. */
+ int position;
+
+ /** Offset between the starting edges of the item and its container. */
+ float offset;
+ }
+
+ private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
+ @Override
+ public int compare(ItemInfo lhs, ItemInfo rhs) {
+ return lhs.position - rhs.position;
+ }
+ };
+
+ private static final Interpolator sInterpolator = new Interpolator() {
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
+ private final ItemInfo mTempItem = new ItemInfo();
+
+ private final Rect mTempRect = new Rect();
+
+ private PagerAdapter mAdapter;
+ private int mCurItem; // Index of currently displayed page.
+ private int mRestoredCurItem = -1;
+ private Parcelable mRestoredAdapterState = null;
+ private ClassLoader mRestoredClassLoader = null;
+ private final Scroller mScroller;
+ private PagerObserver mObserver;
+
+ private int mPageMargin;
+ private Drawable mMarginDrawable;
+ private int mTopPageBounds;
+ private int mBottomPageBounds;
+
+ /**
+ * The increment used to move in the "left" direction. Dependent on layout
+ * direction.
+ */
+ private int mLeftIncr = -1;
+
+ // Offsets of the first and last items, if known.
+ // Set during population, used to determine if we are at the beginning
+ // or end of the pager data set during touch scrolling.
+ private float mFirstOffset = -Float.MAX_VALUE;
+ private float mLastOffset = Float.MAX_VALUE;
+
+ private int mChildWidthMeasureSpec;
+ private int mChildHeightMeasureSpec;
+ private boolean mInLayout;
+
+ private boolean mScrollingCacheEnabled;
+
+ private boolean mPopulatePending;
+ private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
+
+ private boolean mIsBeingDragged;
+ private boolean mIsUnableToDrag;
+ private final int mDefaultGutterSize;
+ private int mGutterSize;
+ private final int mTouchSlop;
+ /**
+ * Position of the last motion event.
+ */
+ private float mLastMotionX;
+ private float mLastMotionY;
+ private float mInitialMotionX;
+ private float mInitialMotionY;
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private int mActivePointerId = INVALID_POINTER;
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+ private final int mMinimumVelocity;
+ private final int mMaximumVelocity;
+ private final int mFlingDistance;
+ private final int mCloseEnough;
+
+ // If the pager is at least this close to its final position, complete the scroll
+ // on touch down and let the user interact with the content inside instead of
+ // "catching" the flinging pager.
+ private static final int CLOSE_ENOUGH = 2; // dp
+
+ private final EdgeEffect mLeftEdge;
+ private final EdgeEffect mRightEdge;
+
+ private boolean mFirstLayout = true;
+ private boolean mCalledSuper;
+ private int mDecorChildCount;
+
+ private OnPageChangeListener mOnPageChangeListener;
+ private OnPageChangeListener mInternalPageChangeListener;
+ private OnAdapterChangeListener mAdapterChangeListener;
+ private PageTransformer mPageTransformer;
+
+ private static final int DRAW_ORDER_DEFAULT = 0;
+ private static final int DRAW_ORDER_FORWARD = 1;
+ private static final int DRAW_ORDER_REVERSE = 2;
+ private int mDrawingOrder;
+ private ArrayList<View> mDrawingOrderedChildren;
+ private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
+
+ /**
+ * Indicates that the pager is in an idle, settled state. The current page
+ * is fully in view and no animation is in progress.
+ */
+ public static final int SCROLL_STATE_IDLE = 0;
+
+ /**
+ * Indicates that the pager is currently being dragged by the user.
+ */
+ public static final int SCROLL_STATE_DRAGGING = 1;
+
+ /**
+ * Indicates that the pager is in the process of settling to a final position.
+ */
+ public static final int SCROLL_STATE_SETTLING = 2;
+
+ private final Runnable mEndScrollRunnable = new Runnable() {
+ public void run() {
+ setScrollState(SCROLL_STATE_IDLE);
+ populate();
+ }
+ };
+
+ private int mScrollState = SCROLL_STATE_IDLE;
+
+ /**
+ * Callback interface for responding to changing state of the selected page.
+ */
+ public interface OnPageChangeListener {
+
+ /**
+ * This method will be invoked when the current page is scrolled, either as part
+ * of a programmatically initiated smooth scroll or a user initiated touch scroll.
+ *
+ * @param position Position index of the first page currently being displayed.
+ * Page position+1 will be visible if positionOffset is nonzero.
+ * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
+ * @param positionOffsetPixels Value in pixels indicating the offset from position.
+ */
+ @UnsupportedAppUsage
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
+
+ /**
+ * This method will be invoked when a new page becomes selected. Animation is not
+ * necessarily complete.
+ *
+ * @param position Position index of the new selected page.
+ */
+ @UnsupportedAppUsage
+ public void onPageSelected(int position);
+
+ /**
+ * Called when the scroll state changes. Useful for discovering when the user
+ * begins dragging, when the pager is automatically settling to the current page,
+ * or when it is fully stopped/idle.
+ *
+ * @param state The new scroll state.
+ * @see com.android.internal.widget.ViewPager#SCROLL_STATE_IDLE
+ * @see com.android.internal.widget.ViewPager#SCROLL_STATE_DRAGGING
+ * @see com.android.internal.widget.ViewPager#SCROLL_STATE_SETTLING
+ */
+ @UnsupportedAppUsage
+ public void onPageScrollStateChanged(int state);
+ }
+
+ /**
+ * Simple implementation of the {@link OnPageChangeListener} interface with stub
+ * implementations of each method. Extend this if you do not intend to override
+ * every method of {@link OnPageChangeListener}.
+ */
+ public static class SimpleOnPageChangeListener implements OnPageChangeListener {
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ // This space for rent
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ // This space for rent
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ // This space for rent
+ }
+ }
+
+ /**
+ * A PageTransformer is invoked whenever a visible/attached page is scrolled.
+ * This offers an opportunity for the application to apply a custom transformation
+ * to the page views using animation properties.
+ *
+ * <p>As property animation is only supported as of Android 3.0 and forward,
+ * setting a PageTransformer on a ViewPager on earlier platform versions will
+ * be ignored.</p>
+ */
+ public interface PageTransformer {
+ /**
+ * Apply a property transformation to the given page.
+ *
+ * @param page Apply the transformation to this page
+ * @param position Position of page relative to the current front-and-center
+ * position of the pager. 0 is front and center. 1 is one full
+ * page position to the right, and -1 is one page position to the left.
+ */
+ public void transformPage(View page, float position);
+ }
+
+ /**
+ * Used internally to monitor when adapters are switched.
+ */
+ interface OnAdapterChangeListener {
+ public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
+ }
+
+ /**
+ * Used internally to tag special types of child views that should be added as
+ * pager decorations by default.
+ */
+ interface Decor {}
+
+ public ViewPager(Context context) {
+ this(context, null);
+ }
+
+ public ViewPager(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ setWillNotDraw(false);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setFocusable(true);
+
+ mScroller = new Scroller(context, sInterpolator);
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ final float density = context.getResources().getDisplayMetrics().density;
+
+ mTouchSlop = configuration.getScaledPagingTouchSlop();
+ mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mLeftEdge = new EdgeEffect(context);
+ mRightEdge = new EdgeEffect(context);
+
+ mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
+ mCloseEnough = (int) (CLOSE_ENOUGH * density);
+ mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
+
+ if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ removeCallbacks(mEndScrollRunnable);
+ super.onDetachedFromWindow();
+ }
+
+ private void setScrollState(int newState) {
+ if (mScrollState == newState) {
+ return;
+ }
+
+ mScrollState = newState;
+ if (mPageTransformer != null) {
+ // PageTransformers can do complex things that benefit from hardware layers.
+ enableLayers(newState != SCROLL_STATE_IDLE);
+ }
+ if (mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageScrollStateChanged(newState);
+ }
+ }
+
+ /**
+ * Set a PagerAdapter that will supply views for this pager as needed.
+ *
+ * @param adapter Adapter to use
+ */
+ public void setAdapter(PagerAdapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ mAdapter.startUpdate(this);
+ for (int i = 0; i < mItems.size(); i++) {
+ final ItemInfo ii = mItems.get(i);
+ mAdapter.destroyItem(this, ii.position, ii.object);
+ }
+ mAdapter.finishUpdate(this);
+ mItems.clear();
+ removeNonDecorViews();
+ mCurItem = 0;
+ scrollTo(0, 0);
+ }
+
+ final PagerAdapter oldAdapter = mAdapter;
+ mAdapter = adapter;
+ mExpectedAdapterCount = 0;
+
+ if (mAdapter != null) {
+ if (mObserver == null) {
+ mObserver = new PagerObserver();
+ }
+ mAdapter.registerDataSetObserver(mObserver);
+ mPopulatePending = false;
+ final boolean wasFirstLayout = mFirstLayout;
+ mFirstLayout = true;
+ mExpectedAdapterCount = mAdapter.getCount();
+ if (mRestoredCurItem >= 0) {
+ mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
+ setCurrentItemInternal(mRestoredCurItem, false, true);
+ mRestoredCurItem = -1;
+ mRestoredAdapterState = null;
+ mRestoredClassLoader = null;
+ } else if (!wasFirstLayout) {
+ populate();
+ } else {
+ requestLayout();
+ }
+ }
+
+ if (mAdapterChangeListener != null && oldAdapter != adapter) {
+ mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
+ }
+ }
+
+ private void removeNonDecorViews() {
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.isDecor) {
+ removeViewAt(i);
+ i--;
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current adapter supplying pages.
+ *
+ * @return The currently registered PagerAdapter
+ */
+ public PagerAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
+ mAdapterChangeListener = listener;
+ }
+
+ private int getPaddedWidth() {
+ return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ }
+
+ /**
+ * Set the currently selected page. If the ViewPager has already been through its first
+ * layout with its current adapter there will be a smooth animated transition between
+ * the current item and the specified item.
+ *
+ * @param item Item index to select
+ */
+ public void setCurrentItem(int item) {
+ mPopulatePending = false;
+ setCurrentItemInternal(item, !mFirstLayout, false);
+ }
+
+ /**
+ * Set the currently selected page.
+ *
+ * @param item Item index to select
+ * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
+ */
+ public void setCurrentItem(int item, boolean smoothScroll) {
+ mPopulatePending = false;
+ setCurrentItemInternal(item, smoothScroll, false);
+ }
+
+ @UnsupportedAppUsage
+ public int getCurrentItem() {
+ return mCurItem;
+ }
+
+ boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
+ return setCurrentItemInternal(item, smoothScroll, always, 0);
+ }
+
+ boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
+ if (mAdapter == null || mAdapter.getCount() <= 0) {
+ setScrollingCacheEnabled(false);
+ return false;
+ }
+
+ item = MathUtils.constrain(item, 0, mAdapter.getCount() - 1);
+ if (!always && mCurItem == item && mItems.size() != 0) {
+ setScrollingCacheEnabled(false);
+ return false;
+ }
+
+ final int pageLimit = mOffscreenPageLimit;
+ if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
+ // We are doing a jump by more than one page. To avoid
+ // glitches, we want to keep all current pages in the view
+ // until the scroll ends.
+ for (int i = 0; i < mItems.size(); i++) {
+ mItems.get(i).scrolling = true;
+ }
+ }
+
+ final boolean dispatchSelected = mCurItem != item;
+ if (mFirstLayout) {
+ // We don't have any idea how big we are yet and shouldn't have any pages either.
+ // Just set things up and let the pending layout handle things.
+ mCurItem = item;
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(item);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(item);
+ }
+ requestLayout();
+ } else {
+ populate(item);
+ scrollToItem(item, smoothScroll, velocity, dispatchSelected);
+ }
+
+ return true;
+ }
+
+ private void scrollToItem(int position, boolean smoothScroll, int velocity,
+ boolean dispatchSelected) {
+ final int destX = getLeftEdgeForItem(position);
+
+ if (smoothScroll) {
+ smoothScrollTo(destX, 0, velocity);
+
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(position);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(position);
+ }
+ } else {
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(position);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(position);
+ }
+
+ completeScroll(false);
+ scrollTo(destX, 0);
+ pageScrolled(destX);
+ }
+ }
+
+ private int getLeftEdgeForItem(int position) {
+ final ItemInfo info = infoForPosition(position);
+ if (info == null) {
+ return 0;
+ }
+
+ final int width = getPaddedWidth();
+ final int scaledOffset = (int) (width * MathUtils.constrain(
+ info.offset, mFirstOffset, mLastOffset));
+
+ if (isLayoutRtl()) {
+ final int itemWidth = (int) (width * info.widthFactor + 0.5f);
+ return MAX_SCROLL_X - itemWidth - scaledOffset;
+ } else {
+ return scaledOffset;
+ }
+ }
+
+ /**
+ * Set a listener that will be invoked whenever the page changes or is incrementally
+ * scrolled. See {@link OnPageChangeListener}.
+ *
+ * @param listener Listener to set
+ */
+ public void setOnPageChangeListener(OnPageChangeListener listener) {
+ mOnPageChangeListener = listener;
+ }
+
+ /**
+ * Set a {@link PageTransformer} that will be called for each attached page whenever
+ * the scroll position is changed. This allows the application to apply custom property
+ * transformations to each page, overriding the default sliding look and feel.
+ *
+ * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
+ * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>
+ *
+ * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
+ * to be drawn from last to first instead of first to last.
+ * @param transformer PageTransformer that will modify each page's animation properties
+ */
+ public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
+ final boolean hasTransformer = transformer != null;
+ final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
+ mPageTransformer = transformer;
+ setChildrenDrawingOrderEnabled(hasTransformer);
+ if (hasTransformer) {
+ mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
+ } else {
+ mDrawingOrder = DRAW_ORDER_DEFAULT;
+ }
+ if (needsPopulate) populate();
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
+ final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
+ return result;
+ }
+
+ /**
+ * Set a separate OnPageChangeListener for internal use by the support library.
+ *
+ * @param listener Listener to set
+ * @return The old listener that was set, if any.
+ */
+ OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
+ OnPageChangeListener oldListener = mInternalPageChangeListener;
+ mInternalPageChangeListener = listener;
+ return oldListener;
+ }
+
+ /**
+ * Returns the number of pages that will be retained to either side of the
+ * current page in the view hierarchy in an idle state. Defaults to 1.
+ *
+ * @return How many pages will be kept offscreen on either side
+ * @see #setOffscreenPageLimit(int)
+ */
+ public int getOffscreenPageLimit() {
+ return mOffscreenPageLimit;
+ }
+
+ /**
+ * Set the number of pages that should be retained to either side of the
+ * current page in the view hierarchy in an idle state. Pages beyond this
+ * limit will be recreated from the adapter when needed.
+ *
+ * <p>This is offered as an optimization. If you know in advance the number
+ * of pages you will need to support or have lazy-loading mechanisms in place
+ * on your pages, tweaking this setting can have benefits in perceived smoothness
+ * of paging animations and interaction. If you have a small number of pages (3-4)
+ * that you can keep active all at once, less time will be spent in layout for
+ * newly created view subtrees as the user pages back and forth.</p>
+ *
+ * <p>You should keep this limit low, especially if your pages have complex layouts.
+ * This setting defaults to 1.</p>
+ *
+ * @param limit How many pages will be kept offscreen in an idle state.
+ */
+ public void setOffscreenPageLimit(int limit) {
+ if (limit < DEFAULT_OFFSCREEN_PAGES) {
+ Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
+ DEFAULT_OFFSCREEN_PAGES);
+ limit = DEFAULT_OFFSCREEN_PAGES;
+ }
+ if (limit != mOffscreenPageLimit) {
+ mOffscreenPageLimit = limit;
+ populate();
+ }
+ }
+
+ /**
+ * Set the margin between pages.
+ *
+ * @param marginPixels Distance between adjacent pages in pixels
+ * @see #getPageMargin()
+ * @see #setPageMarginDrawable(android.graphics.drawable.Drawable)
+ * @see #setPageMarginDrawable(int)
+ */
+ public void setPageMargin(int marginPixels) {
+ final int oldMargin = mPageMargin;
+ mPageMargin = marginPixels;
+
+ final int width = getWidth();
+ recomputeScrollPosition(width, width, marginPixels, oldMargin);
+
+ requestLayout();
+ }
+
+ /**
+ * Return the margin between pages.
+ *
+ * @return The size of the margin in pixels
+ */
+ public int getPageMargin() {
+ return mPageMargin;
+ }
+
+ /**
+ * Set a drawable that will be used to fill the margin between pages.
+ *
+ * @param d Drawable to display between pages
+ */
+ public void setPageMarginDrawable(Drawable d) {
+ mMarginDrawable = d;
+ if (d != null) refreshDrawableState();
+ setWillNotDraw(d == null);
+ invalidate();
+ }
+
+ /**
+ * Set a drawable that will be used to fill the margin between pages.
+ *
+ * @param resId Resource ID of a drawable to display between pages
+ */
+ public void setPageMarginDrawable(@DrawableRes int resId) {
+ setPageMarginDrawable(getContext().getDrawable(resId));
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return super.verifyDrawable(who) || who == mMarginDrawable;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ final Drawable marginDrawable = mMarginDrawable;
+ if (marginDrawable != null && marginDrawable.isStateful()
+ && marginDrawable.setState(getDrawableState())) {
+ invalidateDrawable(marginDrawable);
+ }
+ }
+
+ // We want the duration of the page snap animation to be influenced by the distance that
+ // the screen has to travel, however, we don't want this duration to be effected in a
+ // purely linear fashion. Instead, we use this method to moderate the effect that the distance
+ // of travel has on the overall snap duration.
+ float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ /**
+ * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
+ *
+ * @param x the number of pixels to scroll by on the X axis
+ * @param y the number of pixels to scroll by on the Y axis
+ */
+ void smoothScrollTo(int x, int y) {
+ smoothScrollTo(x, y, 0);
+ }
+
+ /**
+ * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
+ *
+ * @param x the number of pixels to scroll by on the X axis
+ * @param y the number of pixels to scroll by on the Y axis
+ * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
+ */
+ void smoothScrollTo(int x, int y, int velocity) {
+ if (getChildCount() == 0) {
+ // Nothing to do.
+ setScrollingCacheEnabled(false);
+ return;
+ }
+ int sx = getScrollX();
+ int sy = getScrollY();
+ int dx = x - sx;
+ int dy = y - sy;
+ if (dx == 0 && dy == 0) {
+ completeScroll(false);
+ populate();
+ setScrollState(SCROLL_STATE_IDLE);
+ return;
+ }
+
+ setScrollingCacheEnabled(true);
+ setScrollState(SCROLL_STATE_SETTLING);
+
+ final int width = getPaddedWidth();
+ final int halfWidth = width / 2;
+ final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
+ final float distance = halfWidth + halfWidth *
+ distanceInfluenceForSnapDuration(distanceRatio);
+
+ int duration = 0;
+ velocity = Math.abs(velocity);
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
+ final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
+ duration = (int) ((pageDelta + 1) * 100);
+ }
+ duration = Math.min(duration, MAX_SETTLE_DURATION);
+
+ mScroller.startScroll(sx, sy, dx, dy, duration);
+ postInvalidateOnAnimation();
+ }
+
+ ItemInfo addNewItem(int position, int index) {
+ ItemInfo ii = new ItemInfo();
+ ii.position = position;
+ ii.object = mAdapter.instantiateItem(this, position);
+ ii.widthFactor = mAdapter.getPageWidth(position);
+ if (index < 0 || index >= mItems.size()) {
+ mItems.add(ii);
+ } else {
+ mItems.add(index, ii);
+ }
+ return ii;
+ }
+
+ void dataSetChanged() {
+ // This method only gets called if our observer is attached, so mAdapter is non-null.
+
+ final int adapterCount = mAdapter.getCount();
+ mExpectedAdapterCount = adapterCount;
+ boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
+ mItems.size() < adapterCount;
+ int newCurrItem = mCurItem;
+
+ boolean isUpdating = false;
+ for (int i = 0; i < mItems.size(); i++) {
+ final ItemInfo ii = mItems.get(i);
+ final int newPos = mAdapter.getItemPosition(ii.object);
+
+ if (newPos == PagerAdapter.POSITION_UNCHANGED) {
+ continue;
+ }
+
+ if (newPos == PagerAdapter.POSITION_NONE) {
+ mItems.remove(i);
+ i--;
+
+ if (!isUpdating) {
+ mAdapter.startUpdate(this);
+ isUpdating = true;
+ }
+
+ mAdapter.destroyItem(this, ii.position, ii.object);
+ needPopulate = true;
+
+ if (mCurItem == ii.position) {
+ // Keep the current item in the valid range
+ newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
+ needPopulate = true;
+ }
+ continue;
+ }
+
+ if (ii.position != newPos) {
+ if (ii.position == mCurItem) {
+ // Our current item changed position. Follow it.
+ newCurrItem = newPos;
+ }
+
+ ii.position = newPos;
+ needPopulate = true;
+ }
+ }
+
+ if (isUpdating) {
+ mAdapter.finishUpdate(this);
+ }
+
+ Collections.sort(mItems, COMPARATOR);
+
+ if (needPopulate) {
+ // Reset our known page widths; populate will recompute them.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.isDecor) {
+ lp.widthFactor = 0.f;
+ }
+ }
+
+ setCurrentItemInternal(newCurrItem, false, true);
+ requestLayout();
+ }
+ }
+
+ public void populate() {
+ populate(mCurItem);
+ }
+
+ void populate(int newCurrentItem) {
+ ItemInfo oldCurInfo = null;
+ int focusDirection = View.FOCUS_FORWARD;
+ if (mCurItem != newCurrentItem) {
+ focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+ oldCurInfo = infoForPosition(mCurItem);
+ mCurItem = newCurrentItem;
+ }
+
+ if (mAdapter == null) {
+ sortChildDrawingOrder();
+ return;
+ }
+
+ // Bail now if we are waiting to populate. This is to hold off
+ // on creating views from the time the user releases their finger to
+ // fling to a new position until we have finished the scroll to
+ // that position, avoiding glitches from happening at that point.
+ if (mPopulatePending) {
+ if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
+ sortChildDrawingOrder();
+ return;
+ }
+
+ // Also, don't populate until we are attached to a window. This is to
+ // avoid trying to populate before we have restored our view hierarchy
+ // state and conflicting with what is restored.
+ if (getWindowToken() == null) {
+ return;
+ }
+
+ mAdapter.startUpdate(this);
+
+ final int pageLimit = mOffscreenPageLimit;
+ final int startPos = Math.max(0, mCurItem - pageLimit);
+ final int N = mAdapter.getCount();
+ final int endPos = Math.min(N-1, mCurItem + pageLimit);
+
+ if (N != mExpectedAdapterCount) {
+ String resName;
+ try {
+ resName = getResources().getResourceName(getId());
+ } catch (Resources.NotFoundException e) {
+ resName = Integer.toHexString(getId());
+ }
+ throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
+ " contents without calling PagerAdapter#notifyDataSetChanged!" +
+ " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
+ " Pager id: " + resName +
+ " Pager class: " + getClass() +
+ " Problematic adapter: " + mAdapter.getClass());
+ }
+
+ // Locate the currently focused item or add it if needed.
+ int curIndex = -1;
+ ItemInfo curItem = null;
+ for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
+ final ItemInfo ii = mItems.get(curIndex);
+ if (ii.position >= mCurItem) {
+ if (ii.position == mCurItem) curItem = ii;
+ break;
+ }
+ }
+
+ if (curItem == null && N > 0) {
+ curItem = addNewItem(mCurItem, curIndex);
+ }
+
+ // Fill 3x the available width or up to the number of offscreen
+ // pages requested to either side, whichever is larger.
+ // If we have no current item we have no work to do.
+ if (curItem != null) {
+ float extraWidthLeft = 0.f;
+ int itemIndex = curIndex - 1;
+ ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ final int clientWidth = getPaddedWidth();
+ final float leftWidthNeeded = clientWidth <= 0 ? 0 :
+ 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
+ for (int pos = mCurItem - 1; pos >= 0; pos--) {
+ if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
+ if (ii == null) {
+ break;
+ }
+ if (pos == ii.position && !ii.scrolling) {
+ mItems.remove(itemIndex);
+ mAdapter.destroyItem(this, pos, ii.object);
+ if (DEBUG) {
+ Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+ " view: " + ii.object);
+ }
+ itemIndex--;
+ curIndex--;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ }
+ } else if (ii != null && pos == ii.position) {
+ extraWidthLeft += ii.widthFactor;
+ itemIndex--;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ } else {
+ ii = addNewItem(pos, itemIndex + 1);
+ extraWidthLeft += ii.widthFactor;
+ curIndex++;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ }
+ }
+
+ float extraWidthRight = curItem.widthFactor;
+ itemIndex = curIndex + 1;
+ if (extraWidthRight < 2.f) {
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ final float rightWidthNeeded = clientWidth <= 0 ? 0 :
+ (float) getPaddingRight() / (float) clientWidth + 2.f;
+ for (int pos = mCurItem + 1; pos < N; pos++) {
+ if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
+ if (ii == null) {
+ break;
+ }
+ if (pos == ii.position && !ii.scrolling) {
+ mItems.remove(itemIndex);
+ mAdapter.destroyItem(this, pos, ii.object);
+ if (DEBUG) {
+ Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+ " view: " + ii.object);
+ }
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ }
+ } else if (ii != null && pos == ii.position) {
+ extraWidthRight += ii.widthFactor;
+ itemIndex++;
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ } else {
+ ii = addNewItem(pos, itemIndex);
+ itemIndex++;
+ extraWidthRight += ii.widthFactor;
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ }
+ }
+ }
+
+ calculatePageOffsets(curItem, curIndex, oldCurInfo);
+ }
+
+ if (DEBUG) {
+ Log.i(TAG, "Current page list:");
+ for (int i=0; i<mItems.size(); i++) {
+ Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
+ }
+ }
+
+ mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
+
+ mAdapter.finishUpdate(this);
+
+ // Check width measurement of current pages and drawing sort order.
+ // Update LayoutParams as needed.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.childIndex = i;
+ if (!lp.isDecor && lp.widthFactor == 0.f) {
+ // 0 means requery the adapter for this, it doesn't have a valid width.
+ final ItemInfo ii = infoForChild(child);
+ if (ii != null) {
+ lp.widthFactor = ii.widthFactor;
+ lp.position = ii.position;
+ }
+ }
+ }
+ sortChildDrawingOrder();
+
+ if (hasFocus()) {
+ View currentFocused = findFocus();
+ ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
+ if (ii == null || ii.position != mCurItem) {
+ for (int i=0; i<getChildCount(); i++) {
+ View child = getChildAt(i);
+ ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ final Rect focusRect;
+ if (currentFocused == null) {
+ focusRect = null;
+ } else {
+ focusRect = mTempRect;
+ currentFocused.getFocusedRect(mTempRect);
+ offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+ offsetRectIntoDescendantCoords(child, mTempRect);
+ }
+ if (child.requestFocus(focusDirection, focusRect)) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void sortChildDrawingOrder() {
+ if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
+ if (mDrawingOrderedChildren == null) {
+ mDrawingOrderedChildren = new ArrayList<View>();
+ } else {
+ mDrawingOrderedChildren.clear();
+ }
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ mDrawingOrderedChildren.add(child);
+ }
+ Collections.sort(mDrawingOrderedChildren, sPositionComparator);
+ }
+ }
+
+ private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
+ final int N = mAdapter.getCount();
+ final int width = getPaddedWidth();
+ final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
+
+ // Fix up offsets for later layout.
+ if (oldCurInfo != null) {
+ final int oldCurPosition = oldCurInfo.position;
+
+ // Base offsets off of oldCurInfo.
+ if (oldCurPosition < curItem.position) {
+ int itemIndex = 0;
+ float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
+ for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size(); pos++) {
+ ItemInfo ii = mItems.get(itemIndex);
+ while (pos > ii.position && itemIndex < mItems.size() - 1) {
+ itemIndex++;
+ ii = mItems.get(itemIndex);
+ }
+
+ while (pos < ii.position) {
+ // We don't have an item populated for this,
+ // ask the adapter for an offset.
+ offset += mAdapter.getPageWidth(pos) + marginOffset;
+ pos++;
+ }
+
+ ii.offset = offset;
+ offset += ii.widthFactor + marginOffset;
+ }
+ } else if (oldCurPosition > curItem.position) {
+ int itemIndex = mItems.size() - 1;
+ float offset = oldCurInfo.offset;
+ for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) {
+ ItemInfo ii = mItems.get(itemIndex);
+ while (pos < ii.position && itemIndex > 0) {
+ itemIndex--;
+ ii = mItems.get(itemIndex);
+ }
+
+ while (pos > ii.position) {
+ // We don't have an item populated for this,
+ // ask the adapter for an offset.
+ offset -= mAdapter.getPageWidth(pos) + marginOffset;
+ pos--;
+ }
+
+ offset -= ii.widthFactor + marginOffset;
+ ii.offset = offset;
+ }
+ }
+ }
+
+ // Base all offsets off of curItem.
+ final int itemCount = mItems.size();
+ float offset = curItem.offset;
+ int pos = curItem.position - 1;
+ mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
+ mLastOffset = curItem.position == N - 1 ?
+ curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
+
+ // Previous pages
+ for (int i = curIndex - 1; i >= 0; i--, pos--) {
+ final ItemInfo ii = mItems.get(i);
+ while (pos > ii.position) {
+ offset -= mAdapter.getPageWidth(pos--) + marginOffset;
+ }
+ offset -= ii.widthFactor + marginOffset;
+ ii.offset = offset;
+ if (ii.position == 0) mFirstOffset = offset;
+ }
+
+ offset = curItem.offset + curItem.widthFactor + marginOffset;
+ pos = curItem.position + 1;
+
+ // Next pages
+ for (int i = curIndex + 1; i < itemCount; i++, pos++) {
+ final ItemInfo ii = mItems.get(i);
+ while (pos < ii.position) {
+ offset += mAdapter.getPageWidth(pos++) + marginOffset;
+ }
+ if (ii.position == N - 1) {
+ mLastOffset = offset + ii.widthFactor - 1;
+ }
+ ii.offset = offset;
+ offset += ii.widthFactor + marginOffset;
+ }
+ }
+
+ /**
+ * This is the persistent state that is saved by ViewPager. Only needed
+ * if you are creating a sublass of ViewPager that must save its own
+ * state, in which case it should implement a subclass of this which
+ * contains that state.
+ */
+ public static class SavedState extends AbsSavedState {
+ int position;
+ Parcelable adapterState;
+ ClassLoader loader;
+
+ public SavedState(@NonNull Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(position);
+ out.writeParcelable(adapterState, flags);
+ }
+
+ @Override
+ public String toString() {
+ return "FragmentPager.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " position=" + position + "}";
+ }
+
+ public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+ return new SavedState(in, loader);
+ }
+
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in, null);
+ }
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ SavedState(Parcel in, ClassLoader loader) {
+ super(in, loader);
+ if (loader == null) {
+ loader = getClass().getClassLoader();
+ }
+ position = in.readInt();
+ adapterState = in.readParcelable(loader);
+ this.loader = loader;
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.position = mCurItem;
+ if (mAdapter != null) {
+ ss.adapterState = mAdapter.saveState();
+ }
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState ss = (SavedState)state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (mAdapter != null) {
+ mAdapter.restoreState(ss.adapterState, ss.loader);
+ setCurrentItemInternal(ss.position, false, true);
+ } else {
+ mRestoredCurItem = ss.position;
+ mRestoredAdapterState = ss.adapterState;
+ mRestoredClassLoader = ss.loader;
+ }
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (!checkLayoutParams(params)) {
+ params = generateLayoutParams(params);
+ }
+ final LayoutParams lp = (LayoutParams) params;
+ lp.isDecor |= child instanceof Decor;
+ if (mInLayout) {
+ if (lp != null && lp.isDecor) {
+ throw new IllegalStateException("Cannot add pager decor view during layout");
+ }
+ lp.needsMeasure = true;
+ addViewInLayout(child, index, params);
+ } else {
+ super.addView(child, index, params);
+ }
+
+ if (USE_CACHE) {
+ if (child.getVisibility() != GONE) {
+ child.setDrawingCacheEnabled(mScrollingCacheEnabled);
+ } else {
+ child.setDrawingCacheEnabled(false);
+ }
+ }
+ }
+
+ public Object getCurrent() {
+ final ItemInfo itemInfo = infoForPosition(getCurrentItem());
+ return itemInfo == null ? null : itemInfo.object;
+ }
+
+ @Override
+ public void removeView(View view) {
+ if (mInLayout) {
+ removeViewInLayout(view);
+ } else {
+ super.removeView(view);
+ }
+ }
+
+ ItemInfo infoForChild(View child) {
+ for (int i=0; i<mItems.size(); i++) {
+ ItemInfo ii = mItems.get(i);
+ if (mAdapter.isViewFromObject(child, ii.object)) {
+ return ii;
+ }
+ }
+ return null;
+ }
+
+ ItemInfo infoForAnyChild(View child) {
+ ViewParent parent;
+ while ((parent=child.getParent()) != this) {
+ if (parent == null || !(parent instanceof View)) {
+ return null;
+ }
+ child = (View)parent;
+ }
+ return infoForChild(child);
+ }
+
+ ItemInfo infoForPosition(int position) {
+ for (int i = 0; i < mItems.size(); i++) {
+ ItemInfo ii = mItems.get(i);
+ if (ii.position == position) {
+ return ii;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // For simple implementation, our internal size is always 0.
+ // We depend on the container to specify the layout size of
+ // our view. We can't really know what it is since we will be
+ // adding and removing different arbitrary views and do not
+ // want the layout to change as this happens.
+ setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
+ getDefaultSize(0, heightMeasureSpec));
+
+ final int measuredWidth = getMeasuredWidth();
+ final int maxGutterSize = measuredWidth / 10;
+ mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
+
+ // Children are just made to fill our space.
+ int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
+ int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+
+ /*
+ * Make sure all children have been properly measured. Decor views first.
+ * Right now we cheat and make this less complicated by assuming decor
+ * views won't intersect. We will pin to edges based on gravity.
+ */
+ int size = getChildCount();
+ for (int i = 0; i < size; ++i) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp != null && lp.isDecor) {
+ final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ int widthMode = MeasureSpec.AT_MOST;
+ int heightMode = MeasureSpec.AT_MOST;
+ boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
+ boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
+
+ if (consumeVertical) {
+ widthMode = MeasureSpec.EXACTLY;
+ } else if (consumeHorizontal) {
+ heightMode = MeasureSpec.EXACTLY;
+ }
+
+ int widthSize = childWidthSize;
+ int heightSize = childHeightSize;
+ if (lp.width != LayoutParams.WRAP_CONTENT) {
+ widthMode = MeasureSpec.EXACTLY;
+ if (lp.width != LayoutParams.FILL_PARENT) {
+ widthSize = lp.width;
+ }
+ }
+ if (lp.height != LayoutParams.WRAP_CONTENT) {
+ heightMode = MeasureSpec.EXACTLY;
+ if (lp.height != LayoutParams.FILL_PARENT) {
+ heightSize = lp.height;
+ }
+ }
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
+ child.measure(widthSpec, heightSpec);
+
+ if (consumeVertical) {
+ childHeightSize -= child.getMeasuredHeight();
+ } else if (consumeHorizontal) {
+ childWidthSize -= child.getMeasuredWidth();
+ }
+ }
+ }
+ }
+
+ mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
+ mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
+
+ // Make sure we have created all fragments that we need to have shown.
+ mInLayout = true;
+ populate();
+ mInLayout = false;
+
+ // Page views next.
+ size = getChildCount();
+ for (int i = 0; i < size; ++i) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
+ + ": " + mChildWidthMeasureSpec);
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp == null || !lp.isDecor) {
+ final int widthSpec = MeasureSpec.makeMeasureSpec(
+ (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
+ child.measure(widthSpec, mChildHeightMeasureSpec);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ // Make sure scroll position is set correctly.
+ if (w != oldw) {
+ recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
+ }
+ }
+
+ private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
+ if (oldWidth > 0 && !mItems.isEmpty()) {
+ final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin;
+ final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight()
+ + oldMargin;
+ final int xpos = getScrollX();
+ final float pageOffset = (float) xpos / oldWidthWithMargin;
+ final int newOffsetPixels = (int) (pageOffset * widthWithMargin);
+
+ scrollTo(newOffsetPixels, getScrollY());
+ if (!mScroller.isFinished()) {
+ // We now return to your regularly scheduled scroll, already in progress.
+ final int newDuration = mScroller.getDuration() - mScroller.timePassed();
+ ItemInfo targetInfo = infoForPosition(mCurItem);
+ mScroller.startScroll(newOffsetPixels, 0,
+ (int) (targetInfo.offset * width), 0, newDuration);
+ }
+ } else {
+ final ItemInfo ii = infoForPosition(mCurItem);
+ final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
+ final int scrollPos = (int) (scrollOffset *
+ (width - getPaddingLeft() - getPaddingRight()));
+ if (scrollPos != getScrollX()) {
+ completeScroll(false);
+ scrollTo(scrollPos, getScrollY());
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int count = getChildCount();
+ int width = r - l;
+ int height = b - t;
+ int paddingLeft = getPaddingLeft();
+ int paddingTop = getPaddingTop();
+ int paddingRight = getPaddingRight();
+ int paddingBottom = getPaddingBottom();
+ final int scrollX = getScrollX();
+
+ int decorCount = 0;
+
+ // First pass - decor views. We need to do this in two passes so that
+ // we have the proper offsets for non-decor views later.
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ int childLeft = 0;
+ int childTop = 0;
+ if (lp.isDecor) {
+ final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ switch (hgrav) {
+ default:
+ childLeft = paddingLeft;
+ break;
+ case Gravity.LEFT:
+ childLeft = paddingLeft;
+ paddingLeft += child.getMeasuredWidth();
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
+ paddingLeft);
+ break;
+ case Gravity.RIGHT:
+ childLeft = width - paddingRight - child.getMeasuredWidth();
+ paddingRight += child.getMeasuredWidth();
+ break;
+ }
+ switch (vgrav) {
+ default:
+ childTop = paddingTop;
+ break;
+ case Gravity.TOP:
+ childTop = paddingTop;
+ paddingTop += child.getMeasuredHeight();
+ break;
+ case Gravity.CENTER_VERTICAL:
+ childTop = Math.max((height - child.getMeasuredHeight()) / 2,
+ paddingTop);
+ break;
+ case Gravity.BOTTOM:
+ childTop = height - paddingBottom - child.getMeasuredHeight();
+ paddingBottom += child.getMeasuredHeight();
+ break;
+ }
+ childLeft += scrollX;
+ child.layout(childLeft, childTop,
+ childLeft + child.getMeasuredWidth(),
+ childTop + child.getMeasuredHeight());
+ decorCount++;
+ }
+ }
+ }
+
+ final int childWidth = width - paddingLeft - paddingRight;
+ // Page views. Do this once we have the right padding offsets from above.
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.isDecor) {
+ continue;
+ }
+
+ final ItemInfo ii = infoForChild(child);
+ if (ii == null) {
+ continue;
+ }
+
+ if (lp.needsMeasure) {
+ // This was added during layout and needs measurement.
+ // Do it now that we know what we're working with.
+ lp.needsMeasure = false;
+ final int widthSpec = MeasureSpec.makeMeasureSpec(
+ (int) (childWidth * lp.widthFactor),
+ MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(
+ (int) (height - paddingTop - paddingBottom),
+ MeasureSpec.EXACTLY);
+ child.measure(widthSpec, heightSpec);
+ }
+
+ final int childMeasuredWidth = child.getMeasuredWidth();
+ final int startOffset = (int) (childWidth * ii.offset);
+ final int childLeft;
+ if (isLayoutRtl()) {
+ childLeft = MAX_SCROLL_X - paddingRight - startOffset - childMeasuredWidth;
+ } else {
+ childLeft = paddingLeft + startOffset;
+ }
+
+ final int childTop = paddingTop;
+ child.layout(childLeft, childTop, childLeft + childMeasuredWidth,
+ childTop + child.getMeasuredHeight());
+ }
+
+ mTopPageBounds = paddingTop;
+ mBottomPageBounds = height - paddingBottom;
+ mDecorChildCount = decorCount;
+
+ if (mFirstLayout) {
+ scrollToItem(mCurItem, false, 0, false);
+ }
+ mFirstLayout = false;
+ }
+
+ @Override
+ public void computeScroll() {
+ if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
+ final int oldX = getScrollX();
+ final int oldY = getScrollY();
+ final int x = mScroller.getCurrX();
+ final int y = mScroller.getCurrY();
+
+ if (oldX != x || oldY != y) {
+ scrollTo(x, y);
+
+ if (!pageScrolled(x)) {
+ mScroller.abortAnimation();
+ scrollTo(0, y);
+ }
+ }
+
+ // Keep on drawing until the animation has finished.
+ postInvalidateOnAnimation();
+ return;
+ }
+
+ // Done with scroll, clean up state.
+ completeScroll(true);
+ }
+
+ private boolean pageScrolled(int scrollX) {
+ if (mItems.size() == 0) {
+ mCalledSuper = false;
+ onPageScrolled(0, 0, 0);
+ if (!mCalledSuper) {
+ throw new IllegalStateException(
+ "onPageScrolled did not call superclass implementation");
+ }
+ return false;
+ }
+
+ // Translate to scrollX to scrollStart for RTL.
+ final int scrollStart;
+ if (isLayoutRtl()) {
+ scrollStart = MAX_SCROLL_X - scrollX;
+ } else {
+ scrollStart = scrollX;
+ }
+
+ final ItemInfo ii = infoForFirstVisiblePage();
+ final int width = getPaddedWidth();
+ final int widthWithMargin = width + mPageMargin;
+ final float marginOffset = (float) mPageMargin / width;
+ final int currentPage = ii.position;
+ final float pageOffset = (((float) scrollStart / width) - ii.offset) /
+ (ii.widthFactor + marginOffset);
+ final int offsetPixels = (int) (pageOffset * widthWithMargin);
+
+ mCalledSuper = false;
+ onPageScrolled(currentPage, pageOffset, offsetPixels);
+ if (!mCalledSuper) {
+ throw new IllegalStateException(
+ "onPageScrolled did not call superclass implementation");
+ }
+ return true;
+ }
+
+ /**
+ * This method will be invoked when the current page is scrolled, either as part
+ * of a programmatically initiated smooth scroll or a user initiated touch scroll.
+ * If you override this method you must call through to the superclass implementation
+ * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
+ * returns.
+ *
+ * @param position Position index of the first page currently being displayed.
+ * Page position+1 will be visible if positionOffset is nonzero.
+ * @param offset Value from [0, 1) indicating the offset from the page at position.
+ * @param offsetPixels Value in pixels indicating the offset from position.
+ */
+ protected void onPageScrolled(int position, float offset, int offsetPixels) {
+ // Offset any decor views if needed - keep them on-screen at all times.
+ if (mDecorChildCount > 0) {
+ final int scrollX = getScrollX();
+ int paddingLeft = getPaddingLeft();
+ int paddingRight = getPaddingRight();
+ final int width = getWidth();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.isDecor) continue;
+
+ final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ int childLeft = 0;
+ switch (hgrav) {
+ default:
+ childLeft = paddingLeft;
+ break;
+ case Gravity.LEFT:
+ childLeft = paddingLeft;
+ paddingLeft += child.getWidth();
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
+ paddingLeft);
+ break;
+ case Gravity.RIGHT:
+ childLeft = width - paddingRight - child.getMeasuredWidth();
+ paddingRight += child.getMeasuredWidth();
+ break;
+ }
+ childLeft += scrollX;
+
+ final int childOffset = childLeft - child.getLeft();
+ if (childOffset != 0) {
+ child.offsetLeftAndRight(childOffset);
+ }
+ }
+ }
+
+ if (mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+ }
+ if (mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+ }
+
+ if (mPageTransformer != null) {
+ final int scrollX = getScrollX();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (lp.isDecor) continue;
+
+ final float transformPos = (float) (child.getLeft() - scrollX) / getPaddedWidth();
+ mPageTransformer.transformPage(child, transformPos);
+ }
+ }
+
+ mCalledSuper = true;
+ }
+
+ private void completeScroll(boolean postEvents) {
+ boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
+ if (needPopulate) {
+ // Done with scroll, no longer want to cache view drawing.
+ setScrollingCacheEnabled(false);
+ mScroller.abortAnimation();
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+ if (oldX != x || oldY != y) {
+ scrollTo(x, y);
+ }
+ }
+ mPopulatePending = false;
+ for (int i=0; i<mItems.size(); i++) {
+ ItemInfo ii = mItems.get(i);
+ if (ii.scrolling) {
+ needPopulate = true;
+ ii.scrolling = false;
+ }
+ }
+ if (needPopulate) {
+ if (postEvents) {
+ postOnAnimation(mEndScrollRunnable);
+ } else {
+ mEndScrollRunnable.run();
+ }
+ }
+ }
+
+ private boolean isGutterDrag(float x, float dx) {
+ return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
+ }
+
+ private void enableLayers(boolean enable) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final int layerType = enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE;
+ getChildAt(i).setLayerType(layerType, null);
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(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.
+ */
+
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ // Always take care of the touch gesture being complete.
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ // Release the drag.
+ if (DEBUG) Log.v(TAG, "Intercept done!");
+ mIsBeingDragged = false;
+ mIsUnableToDrag = false;
+ mActivePointerId = INVALID_POINTER;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ return false;
+ }
+
+ // Nothing more to do here if we have decided whether or not we
+ // are dragging.
+ if (action != MotionEvent.ACTION_DOWN) {
+ if (mIsBeingDragged) {
+ if (DEBUG) Log.v(TAG, "Being dragged, intercept returning true!");
+ return true;
+ }
+ if (mIsUnableToDrag) {
+ if (DEBUG) Log.v(TAG, "Unable to drag, intercept returning false!");
+ return false;
+ }
+ }
+
+ switch (action) {
+ 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);
+ final float x = ev.getX(pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float xDiff = Math.abs(dx);
+ final float y = ev.getY(pointerIndex);
+ final float yDiff = Math.abs(y - mInitialMotionY);
+ if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+
+ if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
+ canScroll(this, false, (int) dx, (int) x, (int) y)) {
+ // Nested view has scrollable area under this point. Let it be handled there.
+ mLastMotionX = x;
+ mLastMotionY = y;
+ mIsUnableToDrag = true;
+ return false;
+ }
+ if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
+ if (DEBUG) Log.v(TAG, "Starting drag!");
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
+ mInitialMotionX - mTouchSlop;
+ mLastMotionY = y;
+ setScrollingCacheEnabled(true);
+ } else if (yDiff > mTouchSlop) {
+ // The finger has moved enough in the vertical
+ // direction to be counted as a drag... abort
+ // any attempt to drag horizontally, to work correctly
+ // with children that have scrolling containers.
+ if (DEBUG) Log.v(TAG, "Starting unable to drag!");
+ mIsUnableToDrag = true;
+ }
+ if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ if (performDrag(x)) {
+ postInvalidateOnAnimation();
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ /*
+ * Remember location of down touch.
+ * ACTION_DOWN always refers to pointer index 0.
+ */
+ mLastMotionX = mInitialMotionX = ev.getX();
+ mLastMotionY = mInitialMotionY = ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+ mIsUnableToDrag = false;
+
+ mScroller.computeScrollOffset();
+ if (mScrollState == SCROLL_STATE_SETTLING &&
+ Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
+ // Let the user 'catch' the pager as it animates.
+ mScroller.abortAnimation();
+ mPopulatePending = false;
+ populate();
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ } else {
+ completeScroll(false);
+ mIsBeingDragged = false;
+ }
+
+ if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+ + " mIsBeingDragged=" + mIsBeingDragged
+ + "mIsUnableToDrag=" + mIsUnableToDrag);
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ /*
+ * The only time we want to intercept motion events is if we are in the
+ * drag mode.
+ */
+ return mIsBeingDragged;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
+ // Don't handle edge touches immediately -- they may actually belong to one of our
+ // descendants.
+ return false;
+ }
+
+ if (mAdapter == null || mAdapter.getCount() == 0) {
+ // Nothing to present or scroll; nothing to touch.
+ return false;
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+ boolean needsInvalidate = false;
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ mScroller.abortAnimation();
+ mPopulatePending = false;
+ populate();
+
+ // Remember where the motion event started
+ mLastMotionX = mInitialMotionX = ev.getX();
+ mLastMotionY = mInitialMotionY = ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ if (!mIsBeingDragged) {
+ final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ final float x = ev.getX(pointerIndex);
+ final float xDiff = Math.abs(x - mLastMotionX);
+ final float y = ev.getY(pointerIndex);
+ final float yDiff = Math.abs(y - mLastMotionY);
+ if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+ if (xDiff > mTouchSlop && xDiff > yDiff) {
+ if (DEBUG) Log.v(TAG, "Starting drag!");
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
+ mInitialMotionX - mTouchSlop;
+ mLastMotionY = y;
+ setScrollState(SCROLL_STATE_DRAGGING);
+ setScrollingCacheEnabled(true);
+
+ // Disallow Parent Intercept, just in case
+ ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ }
+ // Not else! Note that mIsBeingDragged can be set above.
+ if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ final float x = ev.getX(activePointerIndex);
+ needsInvalidate |= performDrag(x);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mIsBeingDragged) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ final int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
+
+ mPopulatePending = true;
+
+ final float scrollStart = getScrollStart();
+ final float scrolledPages = scrollStart / getPaddedWidth();
+ final ItemInfo ii = infoForFirstVisiblePage();
+ final int currentPage = ii.position;
+ final float nextPageOffset;
+ if (isLayoutRtl()) {
+ nextPageOffset = (ii.offset - scrolledPages) / ii.widthFactor;
+ } else {
+ nextPageOffset = (scrolledPages - ii.offset) / ii.widthFactor;
+ }
+
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ final float x = ev.getX(activePointerIndex);
+ final int totalDelta = (int) (x - mInitialMotionX);
+ final int nextPage = determineTargetPage(
+ currentPage, nextPageOffset, initialVelocity, totalDelta);
+ setCurrentItemInternal(nextPage, true, true, initialVelocity);
+
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ mLeftEdge.onRelease();
+ mRightEdge.onRelease();
+ needsInvalidate = true;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsBeingDragged) {
+ scrollToItem(mCurItem, true, 0, false);
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ mLeftEdge.onRelease();
+ mRightEdge.onRelease();
+ needsInvalidate = true;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int index = ev.getActionIndex();
+ final float x = ev.getX(index);
+ mLastMotionX = x;
+ mActivePointerId = ev.getPointerId(index);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
+ break;
+ }
+ if (needsInvalidate) {
+ postInvalidateOnAnimation();
+ }
+ return true;
+ }
+
+ private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+
+ private boolean performDrag(float x) {
+ boolean needsInvalidate = false;
+
+ final int width = getPaddedWidth();
+ final float deltaX = mLastMotionX - x;
+ mLastMotionX = x;
+
+ final EdgeEffect startEdge;
+ final EdgeEffect endEdge;
+ if (isLayoutRtl()) {
+ startEdge = mRightEdge;
+ endEdge = mLeftEdge;
+ } else {
+ startEdge = mLeftEdge;
+ endEdge = mRightEdge;
+ }
+
+ // Translate scroll to relative coordinates.
+ final float nextScrollX = getScrollX() + deltaX;
+ final float scrollStart;
+ if (isLayoutRtl()) {
+ scrollStart = MAX_SCROLL_X - nextScrollX;
+ } else {
+ scrollStart = nextScrollX;
+ }
+
+ final float startBound;
+ final ItemInfo startItem = mItems.get(0);
+ final boolean startAbsolute = startItem.position == 0;
+ if (startAbsolute) {
+ startBound = startItem.offset * width;
+ } else {
+ startBound = width * mFirstOffset;
+ }
+
+ final float endBound;
+ final ItemInfo endItem = mItems.get(mItems.size() - 1);
+ final boolean endAbsolute = endItem.position == mAdapter.getCount() - 1;
+ if (endAbsolute) {
+ endBound = endItem.offset * width;
+ } else {
+ endBound = width * mLastOffset;
+ }
+
+ final float clampedScrollStart;
+ if (scrollStart < startBound) {
+ if (startAbsolute) {
+ final float over = startBound - scrollStart;
+ startEdge.onPull(Math.abs(over) / width);
+ needsInvalidate = true;
+ }
+ clampedScrollStart = startBound;
+ } else if (scrollStart > endBound) {
+ if (endAbsolute) {
+ final float over = scrollStart - endBound;
+ endEdge.onPull(Math.abs(over) / width);
+ needsInvalidate = true;
+ }
+ clampedScrollStart = endBound;
+ } else {
+ clampedScrollStart = scrollStart;
+ }
+
+ // Translate back to absolute coordinates.
+ final float targetScrollX;
+ if (isLayoutRtl()) {
+ targetScrollX = MAX_SCROLL_X - clampedScrollStart;
+ } else {
+ targetScrollX = clampedScrollStart;
+ }
+
+ // Don't lose the rounded component.
+ mLastMotionX += targetScrollX - (int) targetScrollX;
+
+ scrollTo((int) targetScrollX, getScrollY());
+ pageScrolled((int) targetScrollX);
+
+ return needsInvalidate;
+ }
+
+ /**
+ * @return Info about the page at the current scroll position.
+ * This can be synthetic for a missing middle page; the 'object' field can be null.
+ */
+ private ItemInfo infoForFirstVisiblePage() {
+ final int startOffset = getScrollStart();
+ final int width = getPaddedWidth();
+ final float scrollOffset = width > 0 ? (float) startOffset / width : 0;
+ final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
+
+ int lastPos = -1;
+ float lastOffset = 0.f;
+ float lastWidth = 0.f;
+ boolean first = true;
+ ItemInfo lastItem = null;
+
+ final int N = mItems.size();
+ for (int i = 0; i < N; i++) {
+ ItemInfo ii = mItems.get(i);
+
+ // Seek to position.
+ if (!first && ii.position != lastPos + 1) {
+ // Create a synthetic item for a missing page.
+ ii = mTempItem;
+ ii.offset = lastOffset + lastWidth + marginOffset;
+ ii.position = lastPos + 1;
+ ii.widthFactor = mAdapter.getPageWidth(ii.position);
+ i--;
+ }
+
+ final float offset = ii.offset;
+ final float startBound = offset;
+ if (first || scrollOffset >= startBound) {
+ final float endBound = offset + ii.widthFactor + marginOffset;
+ if (scrollOffset < endBound || i == mItems.size() - 1) {
+ return ii;
+ }
+ } else {
+ return lastItem;
+ }
+
+ first = false;
+ lastPos = ii.position;
+ lastOffset = offset;
+ lastWidth = ii.widthFactor;
+ lastItem = ii;
+ }
+
+ return lastItem;
+ }
+
+ private int getScrollStart() {
+ if (isLayoutRtl()) {
+ return MAX_SCROLL_X - getScrollX();
+ } else {
+ return getScrollX();
+ }
+ }
+
+ /**
+ * @param currentPage the position of the page with the first visible starting edge
+ * @param pageOffset the fraction of the right-hand page that's visible
+ * @param velocity the velocity of the touch event stream
+ * @param deltaX the distance of the touch event stream
+ * @return the position of the target page
+ */
+ private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
+ int targetPage;
+ if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+ targetPage = currentPage - (velocity < 0 ? mLeftIncr : 0);
+ } else {
+ final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
+ targetPage = (int) (currentPage - mLeftIncr * (pageOffset + truncator));
+ }
+
+ if (mItems.size() > 0) {
+ final ItemInfo firstItem = mItems.get(0);
+ final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+
+ // Only let the user target pages we have items for
+ targetPage = MathUtils.constrain(targetPage, firstItem.position, lastItem.position);
+ }
+
+ return targetPage;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ boolean needsInvalidate = false;
+
+ final int overScrollMode = getOverScrollMode();
+ if (overScrollMode == View.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ mAdapter != null && mAdapter.getCount() > 1)) {
+ if (!mLeftEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+ final int width = getWidth();
+
+ canvas.rotate(270);
+ canvas.translate(-height + getPaddingTop(), mFirstOffset * width);
+ mLeftEdge.setSize(height, width);
+ needsInvalidate |= mLeftEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mRightEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.rotate(90);
+ canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);
+ mRightEdge.setSize(height, width);
+ needsInvalidate |= mRightEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ }
+ } else {
+ mLeftEdge.finish();
+ mRightEdge.finish();
+ }
+
+ if (needsInvalidate) {
+ // Keep animating
+ postInvalidateOnAnimation();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Draw the margin drawable between pages if needed.
+ if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
+ final int scrollX = getScrollX();
+ final int width = getWidth();
+
+ final float marginOffset = (float) mPageMargin / width;
+ int itemIndex = 0;
+ ItemInfo ii = mItems.get(0);
+ float offset = ii.offset;
+
+ final int itemCount = mItems.size();
+ final int firstPos = ii.position;
+ final int lastPos = mItems.get(itemCount - 1).position;
+ for (int pos = firstPos; pos < lastPos; pos++) {
+ while (pos > ii.position && itemIndex < itemCount) {
+ ii = mItems.get(++itemIndex);
+ }
+
+ final float itemOffset;
+ final float widthFactor;
+ if (pos == ii.position) {
+ itemOffset = ii.offset;
+ widthFactor = ii.widthFactor;
+ } else {
+ itemOffset = offset;
+ widthFactor = mAdapter.getPageWidth(pos);
+ }
+
+ final float left;
+ final float scaledOffset = itemOffset * width;
+ if (isLayoutRtl()) {
+ left = MAX_SCROLL_X - scaledOffset;
+ } else {
+ left = scaledOffset + widthFactor * width;
+ }
+
+ offset = itemOffset + widthFactor + marginOffset;
+
+ if (left + mPageMargin > scrollX) {
+ mMarginDrawable.setBounds((int) left, mTopPageBounds,
+ (int) (left + mPageMargin + 0.5f), mBottomPageBounds);
+ mMarginDrawable.draw(canvas);
+ }
+
+ if (left > scrollX + width) {
+ break; // No more visible, no sense in continuing
+ }
+ }
+ }
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionX = ev.getX(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+
+ private void endDrag() {
+ mIsBeingDragged = false;
+ mIsUnableToDrag = false;
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void setScrollingCacheEnabled(boolean enabled) {
+ if (mScrollingCacheEnabled != enabled) {
+ mScrollingCacheEnabled = enabled;
+ if (USE_CACHE) {
+ final int size = getChildCount();
+ for (int i = 0; i < size; ++i) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ child.setDrawingCacheEnabled(enabled);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean canScrollHorizontally(int direction) {
+ if (mAdapter == null) {
+ return false;
+ }
+
+ final int width = getPaddedWidth();
+ final int scrollX = getScrollX();
+ if (direction < 0) {
+ return (scrollX > (int) (width * mFirstOffset));
+ } else if (direction > 0) {
+ return (scrollX < (int) (width * mLastOffset));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Tests scrollability within child views of v given a delta of dx.
+ *
+ * @param v View to test for horizontal scrollability
+ * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+ * or just its children (false).
+ * @param dx Delta scrolled in pixels
+ * @param x X coordinate of the active touch point
+ * @param y Y coordinate of the active touch point
+ * @return true if child views of v can be scrolled by delta of dx.
+ */
+ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+ if (v instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) v;
+ final int scrollX = v.getScrollX();
+ final int scrollY = v.getScrollY();
+ final int count = group.getChildCount();
+ // Count backwards - let topmost views consume scroll distance first.
+ for (int i = count - 1; i >= 0; i--) {
+ // TODO: Add support for transformed views.
+ final View child = group.getChildAt(i);
+ if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
+ && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
+ && canScroll(child, true, dx, x + scrollX - child.getLeft(),
+ y + scrollY - child.getTop())) {
+ return true;
+ }
+ }
+ }
+
+ return checkV && v.canScrollHorizontally(-dx);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Let the focused view and/or our descendants get the key first
+ return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+ }
+
+ /**
+ * You can call this function yourself to have the scroll view perform
+ * scrolling from a key event, just as if the event had been dispatched to
+ * it by the view hierarchy.
+ *
+ * @param event The key event to execute.
+ * @return Return true if the event was handled, else false.
+ */
+ public boolean executeKeyEvent(KeyEvent event) {
+ boolean handled = false;
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled = arrowScroll(FOCUS_LEFT);
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled = arrowScroll(FOCUS_RIGHT);
+ break;
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers()) {
+ handled = arrowScroll(FOCUS_FORWARD);
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ handled = arrowScroll(FOCUS_BACKWARD);
+ }
+ break;
+ }
+ }
+ return handled;
+ }
+
+ public boolean arrowScroll(int direction) {
+ View currentFocused = findFocus();
+ if (currentFocused == this) {
+ currentFocused = null;
+ } else if (currentFocused != null) {
+ boolean isChild = false;
+ for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
+ parent = parent.getParent()) {
+ if (parent == this) {
+ isChild = true;
+ break;
+ }
+ }
+ if (!isChild) {
+ // This would cause the focus search down below to fail in fun ways.
+ final StringBuilder sb = new StringBuilder();
+ sb.append(currentFocused.getClass().getSimpleName());
+ for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
+ parent = parent.getParent()) {
+ sb.append(" => ").append(parent.getClass().getSimpleName());
+ }
+ Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
+ "current focused view " + sb.toString());
+ currentFocused = null;
+ }
+ }
+
+ boolean handled = false;
+
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
+ direction);
+ if (nextFocused != null && nextFocused != currentFocused) {
+ if (direction == View.FOCUS_LEFT) {
+ // If there is nothing to the left, or this is causing us to
+ // jump to the right, then what we really want to do is page left.
+ final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
+ final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
+ if (currentFocused != null && nextLeft >= currLeft) {
+ handled = pageLeft();
+ } else {
+ handled = nextFocused.requestFocus();
+ }
+ } else if (direction == View.FOCUS_RIGHT) {
+ // If there is nothing to the right, or this is causing us to
+ // jump to the left, then what we really want to do is page right.
+ final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
+ final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
+ if (currentFocused != null && nextLeft <= currLeft) {
+ handled = pageRight();
+ } else {
+ handled = nextFocused.requestFocus();
+ }
+ }
+ } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
+ // Trying to move left and nothing there; try to page.
+ handled = pageLeft();
+ } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
+ // Trying to move right and nothing there; try to page.
+ handled = pageRight();
+ }
+ if (handled) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ }
+ return handled;
+ }
+
+ private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
+ if (outRect == null) {
+ outRect = new Rect();
+ }
+ if (child == null) {
+ outRect.set(0, 0, 0, 0);
+ return outRect;
+ }
+ outRect.left = child.getLeft();
+ outRect.right = child.getRight();
+ outRect.top = child.getTop();
+ outRect.bottom = child.getBottom();
+
+ ViewParent parent = child.getParent();
+ while (parent instanceof ViewGroup && parent != this) {
+ final ViewGroup group = (ViewGroup) parent;
+ outRect.left += group.getLeft();
+ outRect.right += group.getRight();
+ outRect.top += group.getTop();
+ outRect.bottom += group.getBottom();
+
+ parent = group.getParent();
+ }
+ return outRect;
+ }
+
+ boolean pageLeft() {
+ return setCurrentItemInternal(mCurItem + mLeftIncr, true, false);
+ }
+
+ boolean pageRight() {
+ return setCurrentItemInternal(mCurItem - mLeftIncr, true, false);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ if (layoutDirection == LAYOUT_DIRECTION_LTR) {
+ mLeftIncr = -1;
+ } else {
+ mLeftIncr = 1;
+ }
+ }
+
+ /**
+ * We only want the current page that is being shown to be focusable.
+ */
+ @Override
+ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+ final int focusableCount = views.size();
+
+ final int descendantFocusability = getDescendantFocusability();
+
+ if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ ItemInfo ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ child.addFocusables(views, direction, focusableMode);
+ }
+ }
+ }
+ }
+
+ // we add ourselves (if focusable) in all cases except for when we are
+ // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
+ // to avoid the focus search finding layouts when a more precise search
+ // among the focusable children would be more interesting.
+ if (
+ descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
+ // No focusable descendants
+ (focusableCount == views.size())) {
+ // Note that we can't call the superclass here, because it will
+ // add all views in. So we need to do the same thing View does.
+ if (!isFocusable()) {
+ return;
+ }
+ if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
+ isInTouchMode() && !isFocusableInTouchMode()) {
+ return;
+ }
+ if (views != null) {
+ views.add(this);
+ }
+ }
+ }
+
+ /**
+ * We only want the current page that is being shown to be touchable.
+ */
+ @Override
+ public void addTouchables(ArrayList<View> views) {
+ // Note that we don't call super.addTouchables(), which means that
+ // we don't call View.addTouchables(). This is okay because a ViewPager
+ // is itself not touchable.
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ ItemInfo ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ child.addTouchables(views);
+ }
+ }
+ }
+ }
+
+ /**
+ * We only want the current page that is being shown to be focusable.
+ */
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+ int index;
+ int increment;
+ int end;
+ int count = getChildCount();
+ if ((direction & FOCUS_FORWARD) != 0) {
+ index = 0;
+ increment = 1;
+ end = count;
+ } else {
+ index = count - 1;
+ increment = -1;
+ end = -1;
+ }
+ for (int i = index; i != end; i += increment) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ ItemInfo ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ if (child.requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams();
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return generateDefaultLayoutParams();
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && super.checkLayoutParams(p);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+
+ event.setClassName(ViewPager.class.getName());
+ event.setScrollable(canScroll());
+
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) {
+ event.setItemCount(mAdapter.getCount());
+ event.setFromIndex(mCurItem);
+ event.setToIndex(mCurItem);
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+
+ info.setClassName(ViewPager.class.getName());
+ info.setScrollable(canScroll());
+
+ if (canScrollHorizontally(1)) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_RIGHT);
+ }
+
+ if (canScrollHorizontally(-1)) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_LEFT);
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle args) {
+ if (super.performAccessibilityAction(action, args)) {
+ return true;
+ }
+
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case R.id.accessibilityActionScrollRight:
+ if (canScrollHorizontally(1)) {
+ setCurrentItem(mCurItem + 1);
+ return true;
+ }
+ return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ case R.id.accessibilityActionScrollLeft:
+ if (canScrollHorizontally(-1)) {
+ setCurrentItem(mCurItem - 1);
+ return true;
+ }
+ return false;
+ }
+
+ return false;
+ }
+
+ private boolean canScroll() {
+ return mAdapter != null && mAdapter.getCount() > 1;
+ }
+
+ private class PagerObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ dataSetChanged();
+ }
+ @Override
+ public void onInvalidated() {
+ dataSetChanged();
+ }
+ }
+
+ /**
+ * Layout parameters that should be supplied for views added to a
+ * ViewPager.
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ /**
+ * true if this view is a decoration on the pager itself and not
+ * a view supplied by the adapter.
+ */
+ public boolean isDecor;
+
+ /**
+ * Gravity setting for use on decor views only:
+ * Where to position the view page within the overall ViewPager
+ * container; constants are defined in {@link android.view.Gravity}.
+ */
+ @InspectableProperty(
+ name = "layout_gravity",
+ valueType = InspectableProperty.ValueType.GRAVITY)
+ public int gravity;
+
+ /**
+ * Width as a 0-1 multiplier of the measured pager width
+ */
+ float widthFactor = 0.f;
+
+ /**
+ * true if this view was added during layout and needs to be measured
+ * before being positioned.
+ */
+ boolean needsMeasure;
+
+ /**
+ * Adapter position this view is for if !isDecor
+ */
+ int position;
+
+ /**
+ * Current child index within the ViewPager that this view occupies
+ */
+ int childIndex;
+
+ public LayoutParams() {
+ super(FILL_PARENT, FILL_PARENT);
+ }
+
+ public LayoutParams(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+ gravity = a.getInteger(0, Gravity.TOP);
+ a.recycle();
+ }
+ }
+
+ static class ViewPositionComparator implements Comparator<View> {
+ @Override
+ public int compare(View lhs, View rhs) {
+ final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
+ final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
+ if (llp.isDecor != rlp.isDecor) {
+ return llp.isDecor ? 1 : -1;
+ }
+ return llp.position - rlp.position;
+ }
+ }
+}
diff --git a/com/android/internal/widget/WatchHeaderListView.java b/com/android/internal/widget/WatchHeaderListView.java
new file mode 100644
index 0000000..0654454
--- /dev/null
+++ b/com/android/internal/widget/WatchHeaderListView.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.IdRes;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.HeaderViewListAdapter;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+public class WatchHeaderListView extends ListView {
+ private View mTopPanel;
+
+ public WatchHeaderListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WatchHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public WatchHeaderListView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
+ ArrayList<ListView.FixedViewInfo> headerViewInfos,
+ ArrayList<ListView.FixedViewInfo> footerViewInfos,
+ ListAdapter adapter) {
+ return new WatchHeaderListAdapter(headerViewInfos, footerViewInfos, adapter);
+ }
+
+ @Override
+ public void addView(View child, ViewGroup.LayoutParams params) {
+ if (mTopPanel == null) {
+ setTopPanel(child);
+ } else {
+ throw new IllegalStateException("WatchHeaderListView can host only one header");
+ }
+ }
+
+ public void setTopPanel(View v) {
+ mTopPanel = v;
+ wrapAdapterIfNecessary();
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ super.setAdapter(adapter);
+ wrapAdapterIfNecessary();
+ }
+
+ @Override
+ protected View findViewTraversal(@IdRes int id) {
+ View v = super.findViewTraversal(id);
+ if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
+ return mTopPanel.findViewById(id);
+ }
+ return v;
+ }
+
+ @Override
+ protected View findViewWithTagTraversal(Object tag) {
+ View v = super.findViewWithTagTraversal(tag);
+ if (v == null && mTopPanel != null && !mTopPanel.isRootNamespace()) {
+ return mTopPanel.findViewWithTag(tag);
+ }
+ return v;
+ }
+
+ @Override
+ protected <T extends View> T findViewByPredicateTraversal(
+ Predicate<View> predicate, View childToSkip) {
+ View v = super.findViewByPredicateTraversal(predicate, childToSkip);
+ if (v == null && mTopPanel != null && mTopPanel != childToSkip
+ && !mTopPanel.isRootNamespace()) {
+ return (T) mTopPanel.findViewByPredicate(predicate);
+ }
+ return (T) v;
+ }
+
+ @Override
+ public int getHeaderViewsCount() {
+ return mTopPanel == null ? super.getHeaderViewsCount()
+ : super.getHeaderViewsCount() + (mTopPanel.getVisibility() == GONE ? 0 : 1);
+ }
+
+ private void wrapAdapterIfNecessary() {
+ ListAdapter adapter = getAdapter();
+ if (adapter != null && mTopPanel != null) {
+ if (!(adapter instanceof WatchHeaderListAdapter)) {
+ wrapHeaderListAdapterInternal();
+ }
+
+ ((WatchHeaderListAdapter) getAdapter()).setTopPanel(mTopPanel);
+ dispatchDataSetObserverOnChangedInternal();
+ }
+ }
+
+ private static class WatchHeaderListAdapter extends HeaderViewListAdapter {
+ private View mTopPanel;
+
+ public WatchHeaderListAdapter(
+ ArrayList<ListView.FixedViewInfo> headerViewInfos,
+ ArrayList<ListView.FixedViewInfo> footerViewInfos,
+ ListAdapter adapter) {
+ super(headerViewInfos, footerViewInfos, adapter);
+ }
+
+ public void setTopPanel(View v) {
+ mTopPanel = v;
+ }
+
+ private int getTopPanelCount() {
+ return (mTopPanel == null || mTopPanel.getVisibility() == GONE) ? 0 : 1;
+ }
+
+ @Override
+ public int getCount() {
+ return super.getCount() + getTopPanelCount();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return getTopPanelCount() == 0 && super.areAllItemsEnabled();
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ int topPanelCount = getTopPanelCount();
+ return position < topPanelCount ? false : super.isEnabled(position - topPanelCount);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ int topPanelCount = getTopPanelCount();
+ return position < topPanelCount ? null : super.getItem(position - topPanelCount);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ int numHeaders = getHeadersCount() + getTopPanelCount();
+ if (getWrappedAdapter() != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = getWrappedAdapter().getCount();
+ if (adjPosition < adapterCount) {
+ return getWrappedAdapter().getItemId(adjPosition);
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ int topPanelCount = getTopPanelCount();
+ return position < topPanelCount
+ ? mTopPanel : super.getView(position - topPanelCount, convertView, parent);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int numHeaders = getHeadersCount() + getTopPanelCount();
+ if (getWrappedAdapter() != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = getWrappedAdapter().getCount();
+ if (adjPosition < adapterCount) {
+ return getWrappedAdapter().getItemViewType(adjPosition);
+ }
+ }
+
+ return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ }
+ }
+}
diff --git a/com/android/internal/widget/WatchListDecorLayout.java b/com/android/internal/widget/WatchListDecorLayout.java
new file mode 100644
index 0000000..5b49611
--- /dev/null
+++ b/com/android/internal/widget/WatchListDecorLayout.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.ListView;
+import android.widget.FrameLayout;
+
+import java.util.ArrayList;
+
+
+/**
+ * Layout for the decor for ListViews on watch-type devices with small screens.
+ * <p>
+ * Supports one panel with the gravity set to top, and one panel with gravity set to bottom.
+ * <p>
+ * Use with one ListView child. The top and bottom panels will track the ListView's scrolling.
+ * If there is no ListView child, it will act like a normal FrameLayout.
+ */
+public class WatchListDecorLayout extends FrameLayout
+ implements ViewTreeObserver.OnScrollChangedListener {
+
+ private int mForegroundPaddingLeft = 0;
+ private int mForegroundPaddingTop = 0;
+ private int mForegroundPaddingRight = 0;
+ private int mForegroundPaddingBottom = 0;
+
+ private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+ /** Track the amount the ListView has to scroll up to account for padding change difference. */
+ private int mPendingScroll;
+ private View mBottomPanel;
+ private View mTopPanel;
+ private ListView mListView;
+ private ViewTreeObserver mObserver;
+
+
+ public WatchListDecorLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WatchListDecorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public WatchListDecorLayout(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mPendingScroll = 0;
+
+ for (int i = 0; i < getChildCount(); ++i) {
+ View child = getChildAt(i);
+ if (child instanceof ListView) {
+ if (mListView != null) {
+ throw new IllegalArgumentException("only one ListView child allowed");
+ }
+ mListView = (ListView) child;
+
+ mListView.setNestedScrollingEnabled(true);
+ mObserver = mListView.getViewTreeObserver();
+ mObserver.addOnScrollChangedListener(this);
+ } else {
+ int gravity = (((LayoutParams) child.getLayoutParams()).gravity
+ & Gravity.VERTICAL_GRAVITY_MASK);
+ if (gravity == Gravity.TOP && mTopPanel == null) {
+ mTopPanel = child;
+ } else if (gravity == Gravity.BOTTOM && mBottomPanel == null) {
+ mBottomPanel = child;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mListView = null;
+ mBottomPanel = null;
+ mTopPanel = null;
+ if (mObserver != null) {
+ if (mObserver.isAlive()) {
+ mObserver.removeOnScrollChangedListener(this);
+ }
+ mObserver = null;
+ }
+ }
+
+ private void applyMeasureToChild(View child, int widthMeasureSpec, int heightMeasureSpec) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec;
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ final int width = Math.max(0, getMeasuredWidth()
+ - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
+ - lp.leftMargin - lp.rightMargin);
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ width, MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
+ lp.leftMargin + lp.rightMargin,
+ lp.width);
+ }
+
+ final int childHeightMeasureSpec;
+ if (lp.height == LayoutParams.MATCH_PARENT) {
+ final int height = Math.max(0, getMeasuredHeight()
+ - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
+ - lp.topMargin - lp.bottomMargin);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ height, MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
+ lp.topMargin + lp.bottomMargin,
+ lp.height);
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ private int measureAndGetHeight(View child, int widthMeasureSpec, int heightMeasureSpec) {
+ if (child != null) {
+ if (child.getVisibility() != GONE) {
+ applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec);
+ return child.getMeasuredHeight();
+ } else if (getMeasureAllChildren()) {
+ applyMeasureToChild(mBottomPanel, widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int count = getChildCount();
+
+ final boolean measureMatchParentChildren =
+ MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+ MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+ mMatchParentChildren.clear();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (getMeasureAllChildren() || child.getVisibility() != GONE) {
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ maxWidth = Math.max(maxWidth,
+ child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight,
+ child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+ if (measureMatchParentChildren) {
+ if (lp.width == LayoutParams.MATCH_PARENT ||
+ lp.height == LayoutParams.MATCH_PARENT) {
+ mMatchParentChildren.add(child);
+ }
+ }
+ }
+ }
+
+ // Account for padding too
+ maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
+ maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
+
+ // Check against our minimum height and width
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ // Check against our foreground's minimum height and width
+ final Drawable drawable = getForeground();
+ if (drawable != null) {
+ maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+ maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+ }
+
+ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+ if (mListView != null) {
+ if (mPendingScroll != 0) {
+ mListView.scrollListBy(mPendingScroll);
+ mPendingScroll = 0;
+ }
+
+ int paddingTop = Math.max(mListView.getPaddingTop(),
+ measureAndGetHeight(mTopPanel, widthMeasureSpec, heightMeasureSpec));
+ int paddingBottom = Math.max(mListView.getPaddingBottom(),
+ measureAndGetHeight(mBottomPanel, widthMeasureSpec, heightMeasureSpec));
+
+ if (paddingTop != mListView.getPaddingTop()
+ || paddingBottom != mListView.getPaddingBottom()) {
+ mPendingScroll += mListView.getPaddingTop() - paddingTop;
+ mListView.setPadding(
+ mListView.getPaddingLeft(), paddingTop,
+ mListView.getPaddingRight(), paddingBottom);
+ }
+ }
+
+ count = mMatchParentChildren.size();
+ if (count > 1) {
+ for (int i = 0; i < count; i++) {
+ final View child = mMatchParentChildren.get(i);
+ if (mListView == null || (child != mTopPanel && child != mBottomPanel)) {
+ applyMeasureToChild(child, widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setForegroundGravity(int foregroundGravity) {
+ if (getForegroundGravity() != foregroundGravity) {
+ super.setForegroundGravity(foregroundGravity);
+
+ // calling get* again here because the set above may apply default constraints
+ final Drawable foreground = getForeground();
+ if (getForegroundGravity() == Gravity.FILL && foreground != null) {
+ Rect padding = new Rect();
+ if (foreground.getPadding(padding)) {
+ mForegroundPaddingLeft = padding.left;
+ mForegroundPaddingTop = padding.top;
+ mForegroundPaddingRight = padding.right;
+ mForegroundPaddingBottom = padding.bottom;
+ }
+ } else {
+ mForegroundPaddingLeft = 0;
+ mForegroundPaddingTop = 0;
+ mForegroundPaddingRight = 0;
+ mForegroundPaddingBottom = 0;
+ }
+ }
+ }
+
+ private int getPaddingLeftWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
+ mPaddingLeft + mForegroundPaddingLeft;
+ }
+
+ private int getPaddingRightWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
+ mPaddingRight + mForegroundPaddingRight;
+ }
+
+ private int getPaddingTopWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
+ mPaddingTop + mForegroundPaddingTop;
+ }
+
+ private int getPaddingBottomWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
+ mPaddingBottom + mForegroundPaddingBottom;
+ }
+
+ @Override
+ public void onScrollChanged() {
+ if (mListView == null) {
+ return;
+ }
+
+ if (mTopPanel != null) {
+ if (mListView.getChildCount() > 0) {
+ if (mListView.getFirstVisiblePosition() == 0) {
+ View firstChild = mListView.getChildAt(0);
+ setScrolling(mTopPanel,
+ firstChild.getY() - mTopPanel.getHeight() - mTopPanel.getTop());
+ } else {
+ // shift to hide the frame, last child is not the last position
+ setScrolling(mTopPanel, -mTopPanel.getHeight());
+ }
+ } else {
+ setScrolling(mTopPanel, 0); // no visible child, fallback to default behaviour
+ }
+ }
+
+ if (mBottomPanel != null) {
+ if (mListView.getChildCount() > 0) {
+ if (mListView.getLastVisiblePosition() >= mListView.getCount() - 1) {
+ View lastChild = mListView.getChildAt(mListView.getChildCount() - 1);
+ setScrolling(mBottomPanel, Math.max(
+ 0,
+ lastChild.getY() + lastChild.getHeight() - mBottomPanel.getTop()));
+ } else {
+ // shift to hide the frame, last child is not the last position
+ setScrolling(mBottomPanel, mBottomPanel.getHeight());
+ }
+ } else {
+ setScrolling(mBottomPanel, 0); // no visible child, fallback to default behaviour
+ }
+ }
+ }
+
+ /** Only set scrolling for the panel if there is a change in its translationY. */
+ private void setScrolling(View panel, float translationY) {
+ if (panel.getTranslationY() != translationY) {
+ panel.setTranslationY(translationY);
+ }
+ }
+}
diff --git a/com/android/internal/widget/WeightedLinearLayout.java b/com/android/internal/widget/WeightedLinearLayout.java
new file mode 100644
index 0000000..385a7c3
--- /dev/null
+++ b/com/android/internal/widget/WeightedLinearLayout.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.widget.LinearLayout;
+
+import static android.view.View.MeasureSpec.*;
+import static com.android.internal.R.*;
+
+/**
+ * A special layout when measured in AT_MOST will take up a given percentage of
+ * the available space.
+ */
+public class WeightedLinearLayout extends LinearLayout {
+ private float mMajorWeightMin;
+ private float mMinorWeightMin;
+ private float mMajorWeightMax;
+ private float mMinorWeightMax;
+
+ public WeightedLinearLayout(Context context) {
+ super(context);
+ }
+
+ public WeightedLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, styleable.WeightedLinearLayout);
+
+ mMajorWeightMin = a.getFloat(styleable.WeightedLinearLayout_majorWeightMin, 0.0f);
+ mMinorWeightMin = a.getFloat(styleable.WeightedLinearLayout_minorWeightMin, 0.0f);
+ mMajorWeightMax = a.getFloat(styleable.WeightedLinearLayout_majorWeightMax, 0.0f);
+ mMinorWeightMax = a.getFloat(styleable.WeightedLinearLayout_minorWeightMax, 0.0f);
+
+ a.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+ final int screenWidth = metrics.widthPixels;
+ final boolean isPortrait = screenWidth < metrics.heightPixels;
+
+ final int widthMode = getMode(widthMeasureSpec);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int width = getMeasuredWidth();
+ boolean measure = false;
+
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);
+
+ final float widthWeightMin = isPortrait ? mMinorWeightMin : mMajorWeightMin;
+ final float widthWeightMax = isPortrait ? mMinorWeightMax : mMajorWeightMax;
+ if (widthMode == AT_MOST) {
+ final int weightedMin = (int) (screenWidth * widthWeightMin);
+ final int weightedMax = (int) (screenWidth * widthWeightMin);
+ if (widthWeightMin > 0.0f && width < weightedMin) {
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(weightedMin, EXACTLY);
+ measure = true;
+ } else if (widthWeightMax > 0.0f && width > weightedMax) {
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(weightedMax, EXACTLY);
+ measure = true;
+ }
+ }
+
+ // TODO: Support height?
+
+ if (measure) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+}
diff --git a/com/android/internal/widget/helper/ItemTouchHelper.java b/com/android/internal/widget/helper/ItemTouchHelper.java
new file mode 100644
index 0000000..9636ed8
--- /dev/null
+++ b/com/android/internal/widget/helper/ItemTouchHelper.java
@@ -0,0 +1,2391 @@
+/*
+ * Copyright (C) 2017 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.internal.widget.helper;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+import android.view.animation.Interpolator;
+
+import com.android.internal.R;
+import com.android.internal.widget.LinearLayoutManager;
+import com.android.internal.widget.RecyclerView;
+import com.android.internal.widget.RecyclerView.OnItemTouchListener;
+import com.android.internal.widget.RecyclerView.ViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
+ * <p>
+ * It works with a RecyclerView and a Callback class, which configures what type of interactions
+ * are enabled and also receives events when user performs these actions.
+ * <p>
+ * Depending on which functionality you support, you should override
+ * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or
+ * {@link Callback#onSwiped(ViewHolder, int)}.
+ * <p>
+ * This class is designed to work with any LayoutManager but for certain situations, it can be
+ * optimized for your custom LayoutManager by extending methods in the
+ * {@link ItemTouchHelper.Callback} class or implementing {@link ItemTouchHelper.ViewDropHandler}
+ * interface in your LayoutManager.
+ * <p>
+ * By default, ItemTouchHelper moves the items' translateX/Y properties to reposition them. On
+ * platforms older than Honeycomb, ItemTouchHelper uses canvas translations and View's visibility
+ * property to move items in response to touch events. You can customize these behaviors by
+ * overriding {@link Callback#onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
+ * boolean)}
+ * or {@link Callback#onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
+ * boolean)}.
+ * <p/>
+ * Most of the time, you only need to override <code>onChildDraw</code> but due to limitations of
+ * platform prior to Honeycomb, you may need to implement <code>onChildDrawOver</code> as well.
+ */
+public class ItemTouchHelper extends RecyclerView.ItemDecoration
+ implements RecyclerView.OnChildAttachStateChangeListener {
+
+ /**
+ * Up direction, used for swipe & drag control.
+ */
+ public static final int UP = 1;
+
+ /**
+ * Down direction, used for swipe & drag control.
+ */
+ public static final int DOWN = 1 << 1;
+
+ /**
+ * Left direction, used for swipe & drag control.
+ */
+ public static final int LEFT = 1 << 2;
+
+ /**
+ * Right direction, used for swipe & drag control.
+ */
+ public static final int RIGHT = 1 << 3;
+
+ // If you change these relative direction values, update Callback#convertToAbsoluteDirection,
+ // Callback#convertToRelativeDirection.
+ /**
+ * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout
+ * direction. Used for swipe & drag control.
+ */
+ public static final int START = LEFT << 2;
+
+ /**
+ * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout
+ * direction. Used for swipe & drag control.
+ */
+ public static final int END = RIGHT << 2;
+
+ /**
+ * ItemTouchHelper is in idle state. At this state, either there is no related motion event by
+ * the user or latest motion events have not yet triggered a swipe or drag.
+ */
+ public static final int ACTION_STATE_IDLE = 0;
+
+ /**
+ * A View is currently being swiped.
+ */
+ public static final int ACTION_STATE_SWIPE = 1;
+
+ /**
+ * A View is currently being dragged.
+ */
+ public static final int ACTION_STATE_DRAG = 2;
+
+ /**
+ * Animation type for views which are swiped successfully.
+ */
+ public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1;
+
+ /**
+ * Animation type for views which are not completely swiped thus will animate back to their
+ * original position.
+ */
+ public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2;
+
+ /**
+ * Animation type for views that were dragged and now will animate to their final position.
+ */
+ public static final int ANIMATION_TYPE_DRAG = 1 << 3;
+
+ static final String TAG = "ItemTouchHelper";
+
+ static final boolean DEBUG = false;
+
+ static final int ACTIVE_POINTER_ID_NONE = -1;
+
+ static final int DIRECTION_FLAG_COUNT = 8;
+
+ private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1;
+
+ static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT;
+
+ static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT;
+
+ /**
+ * The unit we are using to track velocity
+ */
+ private static final int PIXELS_PER_SECOND = 1000;
+
+ /**
+ * Views, whose state should be cleared after they are detached from RecyclerView.
+ * This is necessary after swipe dismissing an item. We wait until animator finishes its job
+ * to clean these views.
+ */
+ final List<View> mPendingCleanup = new ArrayList<View>();
+
+ /**
+ * Re-use array to calculate dx dy for a ViewHolder
+ */
+ private final float[] mTmpPosition = new float[2];
+
+ /**
+ * Currently selected view holder
+ */
+ ViewHolder mSelected = null;
+
+ /**
+ * The reference coordinates for the action start. For drag & drop, this is the time long
+ * press is completed vs for swipe, this is the initial touch point.
+ */
+ float mInitialTouchX;
+
+ float mInitialTouchY;
+
+ /**
+ * Set when ItemTouchHelper is assigned to a RecyclerView.
+ */
+ float mSwipeEscapeVelocity;
+
+ /**
+ * Set when ItemTouchHelper is assigned to a RecyclerView.
+ */
+ float mMaxSwipeVelocity;
+
+ /**
+ * The diff between the last event and initial touch.
+ */
+ float mDx;
+
+ float mDy;
+
+ /**
+ * The coordinates of the selected view at the time it is selected. We record these values
+ * when action starts so that we can consistently position it even if LayoutManager moves the
+ * View.
+ */
+ float mSelectedStartX;
+
+ float mSelectedStartY;
+
+ /**
+ * The pointer we are tracking.
+ */
+ int mActivePointerId = ACTIVE_POINTER_ID_NONE;
+
+ /**
+ * Developer callback which controls the behavior of ItemTouchHelper.
+ */
+ Callback mCallback;
+
+ /**
+ * Current mode.
+ */
+ int mActionState = ACTION_STATE_IDLE;
+
+ /**
+ * The direction flags obtained from unmasking
+ * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current
+ * action state.
+ */
+ int mSelectedFlags;
+
+ /**
+ * When a View is dragged or swiped and needs to go back to where it was, we create a Recover
+ * Animation and animate it to its location using this custom Animator, instead of using
+ * framework Animators.
+ * Using framework animators has the side effect of clashing with ItemAnimator, creating
+ * jumpy UIs.
+ */
+ List<RecoverAnimation> mRecoverAnimations = new ArrayList<RecoverAnimation>();
+
+ private int mSlop;
+
+ RecyclerView mRecyclerView;
+
+ /**
+ * When user drags a view to the edge, we start scrolling the LayoutManager as long as View
+ * is partially out of bounds.
+ */
+ final Runnable mScrollRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mSelected != null && scrollIfNecessary()) {
+ if (mSelected != null) { //it might be lost during scrolling
+ moveIfNecessary(mSelected);
+ }
+ mRecyclerView.removeCallbacks(mScrollRunnable);
+ mRecyclerView.postOnAnimation(this);
+ }
+ }
+ };
+
+ /**
+ * Used for detecting fling swipe
+ */
+ VelocityTracker mVelocityTracker;
+
+ //re-used list for selecting a swap target
+ private List<ViewHolder> mSwapTargets;
+
+ //re used for for sorting swap targets
+ private List<Integer> mDistances;
+
+ /**
+ * If drag & drop is supported, we use child drawing order to bring them to front.
+ */
+ private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null;
+
+ /**
+ * This keeps a reference to the child dragged by the user. Even after user stops dragging,
+ * until view reaches its final position (end of recover animation), we keep a reference so
+ * that it can be drawn above other children.
+ */
+ View mOverdrawChild = null;
+
+ /**
+ * We cache the position of the overdraw child to avoid recalculating it each time child
+ * position callback is called. This value is invalidated whenever a child is attached or
+ * detached.
+ */
+ int mOverdrawChildPosition = -1;
+
+ /**
+ * Used to detect long press.
+ */
+ GestureDetector mGestureDetector;
+
+ private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ if (DEBUG) {
+ Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
+ }
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mActivePointerId = event.getPointerId(0);
+ mInitialTouchX = event.getX();
+ mInitialTouchY = event.getY();
+ obtainVelocityTracker();
+ if (mSelected == null) {
+ final RecoverAnimation animation = findAnimation(event);
+ if (animation != null) {
+ mInitialTouchX -= animation.mX;
+ mInitialTouchY -= animation.mY;
+ endRecoverAnimation(animation.mViewHolder, true);
+ if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
+ mCallback.clearView(mRecyclerView, animation.mViewHolder);
+ }
+ select(animation.mViewHolder, animation.mActionState);
+ updateDxDy(event, mSelectedFlags, 0);
+ }
+ }
+ } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mActivePointerId = ACTIVE_POINTER_ID_NONE;
+ select(null, ACTION_STATE_IDLE);
+ } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
+ // in a non scroll orientation, if distance change is above threshold, we
+ // can select the item
+ final int index = event.findPointerIndex(mActivePointerId);
+ if (DEBUG) {
+ Log.d(TAG, "pointer index " + index);
+ }
+ if (index >= 0) {
+ checkSelectForSwipe(action, event, index);
+ }
+ }
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(event);
+ }
+ return mSelected != null;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ if (DEBUG) {
+ Log.d(TAG,
+ "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
+ }
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(event);
+ }
+ if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
+ return;
+ }
+ final int action = event.getActionMasked();
+ final int activePointerIndex = event.findPointerIndex(mActivePointerId);
+ if (activePointerIndex >= 0) {
+ checkSelectForSwipe(action, event, activePointerIndex);
+ }
+ ViewHolder viewHolder = mSelected;
+ if (viewHolder == null) {
+ return;
+ }
+ switch (action) {
+ case MotionEvent.ACTION_MOVE: {
+ // Find the index of the active pointer and fetch its position
+ if (activePointerIndex >= 0) {
+ updateDxDy(event, mSelectedFlags, activePointerIndex);
+ moveIfNecessary(viewHolder);
+ mRecyclerView.removeCallbacks(mScrollRunnable);
+ mScrollRunnable.run();
+ mRecyclerView.invalidate();
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ // fall through
+ case MotionEvent.ACTION_UP:
+ select(null, ACTION_STATE_IDLE);
+ mActivePointerId = ACTIVE_POINTER_ID_NONE;
+ break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ final int pointerIndex = event.getActionIndex();
+ final int pointerId = event.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mActivePointerId = event.getPointerId(newPointerIndex);
+ updateDxDy(event, mSelectedFlags, pointerIndex);
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (!disallowIntercept) {
+ return;
+ }
+ select(null, ACTION_STATE_IDLE);
+ }
+ };
+
+ /**
+ * Temporary rect instance that is used when we need to lookup Item decorations.
+ */
+ private Rect mTmpRect;
+
+ /**
+ * When user started to drag scroll. Reset when we don't scroll
+ */
+ private long mDragScrollStartTimeInMs;
+
+ /**
+ * Creates an ItemTouchHelper that will work with the given Callback.
+ * <p>
+ * You can attach ItemTouchHelper to a RecyclerView via
+ * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration,
+ * an onItemTouchListener and a Child attach / detach listener to the RecyclerView.
+ *
+ * @param callback The Callback which controls the behavior of this touch helper.
+ */
+ public ItemTouchHelper(Callback callback) {
+ mCallback = callback;
+ }
+
+ private static boolean hitTest(View child, float x, float y, float left, float top) {
+ return x >= left
+ && x <= left + child.getWidth()
+ && y >= top
+ && y <= top + child.getHeight();
+ }
+
+ /**
+ * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already
+ * attached to a RecyclerView, it will first detach from the previous one. You can call this
+ * method with {@code null} to detach it from the current RecyclerView.
+ *
+ * @param recyclerView The RecyclerView instance to which you want to add this helper or
+ * {@code null} if you want to remove ItemTouchHelper from the current
+ * RecyclerView.
+ */
+ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
+ if (mRecyclerView == recyclerView) {
+ return; // nothing to do
+ }
+ if (mRecyclerView != null) {
+ destroyCallbacks();
+ }
+ mRecyclerView = recyclerView;
+ if (mRecyclerView != null) {
+ final Resources resources = recyclerView.getResources();
+ mSwipeEscapeVelocity = resources
+ .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
+ mMaxSwipeVelocity = resources
+ .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
+ setupCallbacks();
+ }
+ }
+
+ private void setupCallbacks() {
+ ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
+ mSlop = vc.getScaledTouchSlop();
+ mRecyclerView.addItemDecoration(this);
+ mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
+ mRecyclerView.addOnChildAttachStateChangeListener(this);
+ initGestureDetector();
+ }
+
+ private void destroyCallbacks() {
+ mRecyclerView.removeItemDecoration(this);
+ mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener);
+ mRecyclerView.removeOnChildAttachStateChangeListener(this);
+ // clean all attached
+ final int recoverAnimSize = mRecoverAnimations.size();
+ for (int i = recoverAnimSize - 1; i >= 0; i--) {
+ final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0);
+ mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
+ }
+ mRecoverAnimations.clear();
+ mOverdrawChild = null;
+ mOverdrawChildPosition = -1;
+ releaseVelocityTracker();
+ }
+
+ private void initGestureDetector() {
+ if (mGestureDetector != null) {
+ return;
+ }
+ mGestureDetector = new GestureDetector(mRecyclerView.getContext(),
+ new ItemTouchHelperGestureListener());
+ }
+
+ private void getSelectedDxDy(float[] outPosition) {
+ if ((mSelectedFlags & (LEFT | RIGHT)) != 0) {
+ outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft();
+ } else {
+ outPosition[0] = mSelected.itemView.getTranslationX();
+ }
+ if ((mSelectedFlags & (UP | DOWN)) != 0) {
+ outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop();
+ } else {
+ outPosition[1] = mSelected.itemView.getTranslationY();
+ }
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ float dx = 0, dy = 0;
+ if (mSelected != null) {
+ getSelectedDxDy(mTmpPosition);
+ dx = mTmpPosition[0];
+ dy = mTmpPosition[1];
+ }
+ mCallback.onDrawOver(c, parent, mSelected,
+ mRecoverAnimations, mActionState, dx, dy);
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ // we don't know if RV changed something so we should invalidate this index.
+ mOverdrawChildPosition = -1;
+ float dx = 0, dy = 0;
+ if (mSelected != null) {
+ getSelectedDxDy(mTmpPosition);
+ dx = mTmpPosition[0];
+ dy = mTmpPosition[1];
+ }
+ mCallback.onDraw(c, parent, mSelected,
+ mRecoverAnimations, mActionState, dx, dy);
+ }
+
+ /**
+ * Starts dragging or swiping the given View. Call with null if you want to clear it.
+ *
+ * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the
+ * current action
+ * @param actionState The type of action
+ */
+ void select(ViewHolder selected, int actionState) {
+ if (selected == mSelected && actionState == mActionState) {
+ return;
+ }
+ mDragScrollStartTimeInMs = Long.MIN_VALUE;
+ final int prevActionState = mActionState;
+ // prevent duplicate animations
+ endRecoverAnimation(selected, true);
+ mActionState = actionState;
+ if (actionState == ACTION_STATE_DRAG) {
+ // we remove after animation is complete. this means we only elevate the last drag
+ // child but that should perform good enough as it is very hard to start dragging a
+ // new child before the previous one settles.
+ mOverdrawChild = selected.itemView;
+ addChildDrawingOrderCallback();
+ }
+ int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
+ - 1;
+ boolean preventLayout = false;
+
+ if (mSelected != null) {
+ final ViewHolder prevSelected = mSelected;
+ if (prevSelected.itemView.getParent() != null) {
+ final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
+ : swipeIfNecessary(prevSelected);
+ releaseVelocityTracker();
+ // find where we should animate to
+ final float targetTranslateX, targetTranslateY;
+ int animationType;
+ switch (swipeDir) {
+ case LEFT:
+ case RIGHT:
+ case START:
+ case END:
+ targetTranslateY = 0;
+ targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
+ break;
+ case UP:
+ case DOWN:
+ targetTranslateX = 0;
+ targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
+ break;
+ default:
+ targetTranslateX = 0;
+ targetTranslateY = 0;
+ }
+ if (prevActionState == ACTION_STATE_DRAG) {
+ animationType = ANIMATION_TYPE_DRAG;
+ } else if (swipeDir > 0) {
+ animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
+ } else {
+ animationType = ANIMATION_TYPE_SWIPE_CANCEL;
+ }
+ getSelectedDxDy(mTmpPosition);
+ final float currentTranslateX = mTmpPosition[0];
+ final float currentTranslateY = mTmpPosition[1];
+ final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
+ prevActionState, currentTranslateX, currentTranslateY,
+ targetTranslateX, targetTranslateY) {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (this.mOverridden) {
+ return;
+ }
+ if (swipeDir <= 0) {
+ // this is a drag or failed swipe. recover immediately
+ mCallback.clearView(mRecyclerView, prevSelected);
+ // full cleanup will happen on onDrawOver
+ } else {
+ // wait until remove animation is complete.
+ mPendingCleanup.add(prevSelected.itemView);
+ mIsPendingCleanup = true;
+ if (swipeDir > 0) {
+ // Animation might be ended by other animators during a layout.
+ // We defer callback to avoid editing adapter during a layout.
+ postDispatchSwipe(this, swipeDir);
+ }
+ }
+ // removed from the list after it is drawn for the last time
+ if (mOverdrawChild == prevSelected.itemView) {
+ removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
+ }
+ }
+ };
+ final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
+ targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
+ rv.setDuration(duration);
+ mRecoverAnimations.add(rv);
+ rv.start();
+ preventLayout = true;
+ } else {
+ removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
+ mCallback.clearView(mRecyclerView, prevSelected);
+ }
+ mSelected = null;
+ }
+ if (selected != null) {
+ mSelectedFlags =
+ (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
+ >> (mActionState * DIRECTION_FLAG_COUNT);
+ mSelectedStartX = selected.itemView.getLeft();
+ mSelectedStartY = selected.itemView.getTop();
+ mSelected = selected;
+
+ if (actionState == ACTION_STATE_DRAG) {
+ mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ }
+ final ViewParent rvParent = mRecyclerView.getParent();
+ if (rvParent != null) {
+ rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
+ }
+ if (!preventLayout) {
+ mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
+ }
+ mCallback.onSelectedChanged(mSelected, mActionState);
+ mRecyclerView.invalidate();
+ }
+
+ void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) {
+ // wait until animations are complete.
+ mRecyclerView.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mRecyclerView != null && mRecyclerView.isAttachedToWindow()
+ && !anim.mOverridden
+ && anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) {
+ final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator();
+ // if animator is running or we have other active recover animations, we try
+ // not to call onSwiped because DefaultItemAnimator is not good at merging
+ // animations. Instead, we wait and batch.
+ if ((animator == null || !animator.isRunning(null))
+ && !hasRunningRecoverAnim()) {
+ mCallback.onSwiped(anim.mViewHolder, swipeDir);
+ } else {
+ mRecyclerView.post(this);
+ }
+ }
+ }
+ });
+ }
+
+ boolean hasRunningRecoverAnim() {
+ final int size = mRecoverAnimations.size();
+ for (int i = 0; i < size; i++) {
+ if (!mRecoverAnimations.get(i).mEnded) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * If user drags the view to the edge, trigger a scroll if necessary.
+ */
+ boolean scrollIfNecessary() {
+ if (mSelected == null) {
+ mDragScrollStartTimeInMs = Long.MIN_VALUE;
+ return false;
+ }
+ final long now = System.currentTimeMillis();
+ final long scrollDuration = mDragScrollStartTimeInMs
+ == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs;
+ RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
+ if (mTmpRect == null) {
+ mTmpRect = new Rect();
+ }
+ int scrollX = 0;
+ int scrollY = 0;
+ lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect);
+ if (lm.canScrollHorizontally()) {
+ int curX = (int) (mSelectedStartX + mDx);
+ final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft();
+ if (mDx < 0 && leftDiff < 0) {
+ scrollX = leftDiff;
+ } else if (mDx > 0) {
+ final int rightDiff =
+ curX + mSelected.itemView.getWidth() + mTmpRect.right
+ - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight());
+ if (rightDiff > 0) {
+ scrollX = rightDiff;
+ }
+ }
+ }
+ if (lm.canScrollVertically()) {
+ int curY = (int) (mSelectedStartY + mDy);
+ final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop();
+ if (mDy < 0 && topDiff < 0) {
+ scrollY = topDiff;
+ } else if (mDy > 0) {
+ final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom
+ - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom());
+ if (bottomDiff > 0) {
+ scrollY = bottomDiff;
+ }
+ }
+ }
+ if (scrollX != 0) {
+ scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
+ mSelected.itemView.getWidth(), scrollX,
+ mRecyclerView.getWidth(), scrollDuration);
+ }
+ if (scrollY != 0) {
+ scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
+ mSelected.itemView.getHeight(), scrollY,
+ mRecyclerView.getHeight(), scrollDuration);
+ }
+ if (scrollX != 0 || scrollY != 0) {
+ if (mDragScrollStartTimeInMs == Long.MIN_VALUE) {
+ mDragScrollStartTimeInMs = now;
+ }
+ mRecyclerView.scrollBy(scrollX, scrollY);
+ return true;
+ }
+ mDragScrollStartTimeInMs = Long.MIN_VALUE;
+ return false;
+ }
+
+ private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) {
+ if (mSwapTargets == null) {
+ mSwapTargets = new ArrayList<ViewHolder>();
+ mDistances = new ArrayList<Integer>();
+ } else {
+ mSwapTargets.clear();
+ mDistances.clear();
+ }
+ final int margin = mCallback.getBoundingBoxMargin();
+ final int left = Math.round(mSelectedStartX + mDx) - margin;
+ final int top = Math.round(mSelectedStartY + mDy) - margin;
+ final int right = left + viewHolder.itemView.getWidth() + 2 * margin;
+ final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin;
+ final int centerX = (left + right) / 2;
+ final int centerY = (top + bottom) / 2;
+ final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
+ final int childCount = lm.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View other = lm.getChildAt(i);
+ if (other == viewHolder.itemView) {
+ continue; //myself!
+ }
+ if (other.getBottom() < top || other.getTop() > bottom
+ || other.getRight() < left || other.getLeft() > right) {
+ continue;
+ }
+ final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other);
+ if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) {
+ // find the index to add
+ final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2);
+ final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2);
+ final int dist = dx * dx + dy * dy;
+
+ int pos = 0;
+ final int cnt = mSwapTargets.size();
+ for (int j = 0; j < cnt; j++) {
+ if (dist > mDistances.get(j)) {
+ pos++;
+ } else {
+ break;
+ }
+ }
+ mSwapTargets.add(pos, otherVh);
+ mDistances.add(pos, dist);
+ }
+ }
+ return mSwapTargets;
+ }
+
+ /**
+ * Checks if we should swap w/ another view holder.
+ */
+ void moveIfNecessary(ViewHolder viewHolder) {
+ if (mRecyclerView.isLayoutRequested()) {
+ return;
+ }
+ if (mActionState != ACTION_STATE_DRAG) {
+ return;
+ }
+
+ final float threshold = mCallback.getMoveThreshold(viewHolder);
+ final int x = (int) (mSelectedStartX + mDx);
+ final int y = (int) (mSelectedStartY + mDy);
+ if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
+ && Math.abs(x - viewHolder.itemView.getLeft())
+ < viewHolder.itemView.getWidth() * threshold) {
+ return;
+ }
+ List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
+ if (swapTargets.size() == 0) {
+ return;
+ }
+ // may swap.
+ ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
+ if (target == null) {
+ mSwapTargets.clear();
+ mDistances.clear();
+ return;
+ }
+ final int toPosition = target.getAdapterPosition();
+ final int fromPosition = viewHolder.getAdapterPosition();
+ if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
+ // keep target visible
+ mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
+ target, toPosition, x, y);
+ }
+ }
+
+ @Override
+ public void onChildViewAttachedToWindow(View view) {
+ }
+
+ @Override
+ public void onChildViewDetachedFromWindow(View view) {
+ removeChildDrawingOrderCallbackIfNecessary(view);
+ final ViewHolder holder = mRecyclerView.getChildViewHolder(view);
+ if (holder == null) {
+ return;
+ }
+ if (mSelected != null && holder == mSelected) {
+ select(null, ACTION_STATE_IDLE);
+ } else {
+ endRecoverAnimation(holder, false); // this may push it into pending cleanup list.
+ if (mPendingCleanup.remove(holder.itemView)) {
+ mCallback.clearView(mRecyclerView, holder);
+ }
+ }
+ }
+
+ /**
+ * Returns the animation type or 0 if cannot be found.
+ */
+ int endRecoverAnimation(ViewHolder viewHolder, boolean override) {
+ final int recoverAnimSize = mRecoverAnimations.size();
+ for (int i = recoverAnimSize - 1; i >= 0; i--) {
+ final RecoverAnimation anim = mRecoverAnimations.get(i);
+ if (anim.mViewHolder == viewHolder) {
+ anim.mOverridden |= override;
+ if (!anim.mEnded) {
+ anim.cancel();
+ }
+ mRecoverAnimations.remove(i);
+ return anim.mAnimationType;
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ outRect.setEmpty();
+ }
+
+ void obtainVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ }
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ private void releaseVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private ViewHolder findSwipedView(MotionEvent motionEvent) {
+ final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
+ if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
+ return null;
+ }
+ final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId);
+ final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX;
+ final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY;
+ final float absDx = Math.abs(dx);
+ final float absDy = Math.abs(dy);
+
+ if (absDx < mSlop && absDy < mSlop) {
+ return null;
+ }
+ if (absDx > absDy && lm.canScrollHorizontally()) {
+ return null;
+ } else if (absDy > absDx && lm.canScrollVertically()) {
+ return null;
+ }
+ View child = findChildView(motionEvent);
+ if (child == null) {
+ return null;
+ }
+ return mRecyclerView.getChildViewHolder(child);
+ }
+
+ /**
+ * Checks whether we should select a View for swiping.
+ */
+ boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
+ if (mSelected != null || action != MotionEvent.ACTION_MOVE
+ || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
+ return false;
+ }
+ if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
+ return false;
+ }
+ final ViewHolder vh = findSwipedView(motionEvent);
+ if (vh == null) {
+ return false;
+ }
+ final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
+
+ final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
+ >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
+
+ if (swipeFlags == 0) {
+ return false;
+ }
+
+ // mDx and mDy are only set in allowed directions. We use custom x/y here instead of
+ // updateDxDy to avoid swiping if user moves more in the other direction
+ final float x = motionEvent.getX(pointerIndex);
+ final float y = motionEvent.getY(pointerIndex);
+
+ // Calculate the distance moved
+ final float dx = x - mInitialTouchX;
+ final float dy = y - mInitialTouchY;
+ // swipe target is chose w/o applying flags so it does not really check if swiping in that
+ // direction is allowed. This why here, we use mDx mDy to check slope value again.
+ final float absDx = Math.abs(dx);
+ final float absDy = Math.abs(dy);
+
+ if (absDx < mSlop && absDy < mSlop) {
+ return false;
+ }
+ if (absDx > absDy) {
+ if (dx < 0 && (swipeFlags & LEFT) == 0) {
+ return false;
+ }
+ if (dx > 0 && (swipeFlags & RIGHT) == 0) {
+ return false;
+ }
+ } else {
+ if (dy < 0 && (swipeFlags & UP) == 0) {
+ return false;
+ }
+ if (dy > 0 && (swipeFlags & DOWN) == 0) {
+ return false;
+ }
+ }
+ mDx = mDy = 0f;
+ mActivePointerId = motionEvent.getPointerId(0);
+ select(vh, ACTION_STATE_SWIPE);
+ return true;
+ }
+
+ View findChildView(MotionEvent event) {
+ // first check elevated views, if none, then call RV
+ final float x = event.getX();
+ final float y = event.getY();
+ if (mSelected != null) {
+ final View selectedView = mSelected.itemView;
+ if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
+ return selectedView;
+ }
+ }
+ for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
+ final RecoverAnimation anim = mRecoverAnimations.get(i);
+ final View view = anim.mViewHolder.itemView;
+ if (hitTest(view, x, y, anim.mX, anim.mY)) {
+ return view;
+ }
+ }
+ return mRecyclerView.findChildViewUnder(x, y);
+ }
+
+ /**
+ * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a
+ * View is long pressed. You can disable that behavior by overriding
+ * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}.
+ * <p>
+ * For this method to work:
+ * <ul>
+ * <li>The provided ViewHolder must be a child of the RecyclerView to which this
+ * ItemTouchHelper
+ * is attached.</li>
+ * <li>{@link ItemTouchHelper.Callback} must have dragging enabled.</li>
+ * <li>There must be a previous touch event that was reported to the ItemTouchHelper
+ * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener
+ * grabs previous events, this should work as expected.</li>
+ * </ul>
+ *
+ * For example, if you would like to let your user to be able to drag an Item by touching one
+ * of its descendants, you may implement it as follows:
+ * <pre>
+ * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
+ * public boolean onTouch(View v, MotionEvent event) {
+ * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
+ * mItemTouchHelper.startDrag(viewHolder);
+ * }
+ * return false;
+ * }
+ * });
+ * </pre>
+ * <p>
+ *
+ * @param viewHolder The ViewHolder to start dragging. It must be a direct child of
+ * RecyclerView.
+ * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled()
+ */
+ public void startDrag(ViewHolder viewHolder) {
+ if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) {
+ Log.e(TAG, "Start drag has been called but dragging is not enabled");
+ return;
+ }
+ if (viewHolder.itemView.getParent() != mRecyclerView) {
+ Log.e(TAG, "Start drag has been called with a view holder which is not a child of "
+ + "the RecyclerView which is controlled by this ItemTouchHelper.");
+ return;
+ }
+ obtainVelocityTracker();
+ mDx = mDy = 0f;
+ select(viewHolder, ACTION_STATE_DRAG);
+ }
+
+ /**
+ * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View
+ * when user swipes their finger (or mouse pointer) over the View. You can disable this
+ * behavior
+ * by overriding {@link ItemTouchHelper.Callback}
+ * <p>
+ * For this method to work:
+ * <ul>
+ * <li>The provided ViewHolder must be a child of the RecyclerView to which this
+ * ItemTouchHelper is attached.</li>
+ * <li>{@link ItemTouchHelper.Callback} must have swiping enabled.</li>
+ * <li>There must be a previous touch event that was reported to the ItemTouchHelper
+ * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener
+ * grabs previous events, this should work as expected.</li>
+ * </ul>
+ *
+ * For example, if you would like to let your user to be able to swipe an Item by touching one
+ * of its descendants, you may implement it as follows:
+ * <pre>
+ * viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
+ * public boolean onTouch(View v, MotionEvent event) {
+ * if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
+ * mItemTouchHelper.startSwipe(viewHolder);
+ * }
+ * return false;
+ * }
+ * });
+ * </pre>
+ *
+ * @param viewHolder The ViewHolder to start swiping. It must be a direct child of
+ * RecyclerView.
+ */
+ public void startSwipe(ViewHolder viewHolder) {
+ if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) {
+ Log.e(TAG, "Start swipe has been called but swiping is not enabled");
+ return;
+ }
+ if (viewHolder.itemView.getParent() != mRecyclerView) {
+ Log.e(TAG, "Start swipe has been called with a view holder which is not a child of "
+ + "the RecyclerView controlled by this ItemTouchHelper.");
+ return;
+ }
+ obtainVelocityTracker();
+ mDx = mDy = 0f;
+ select(viewHolder, ACTION_STATE_SWIPE);
+ }
+
+ RecoverAnimation findAnimation(MotionEvent event) {
+ if (mRecoverAnimations.isEmpty()) {
+ return null;
+ }
+ View target = findChildView(event);
+ for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
+ final RecoverAnimation anim = mRecoverAnimations.get(i);
+ if (anim.mViewHolder.itemView == target) {
+ return anim;
+ }
+ }
+ return null;
+ }
+
+ void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
+ final float x = ev.getX(pointerIndex);
+ final float y = ev.getY(pointerIndex);
+
+ // Calculate the distance moved
+ mDx = x - mInitialTouchX;
+ mDy = y - mInitialTouchY;
+ if ((directionFlags & LEFT) == 0) {
+ mDx = Math.max(0, mDx);
+ }
+ if ((directionFlags & RIGHT) == 0) {
+ mDx = Math.min(0, mDx);
+ }
+ if ((directionFlags & UP) == 0) {
+ mDy = Math.max(0, mDy);
+ }
+ if ((directionFlags & DOWN) == 0) {
+ mDy = Math.min(0, mDy);
+ }
+ }
+
+ private int swipeIfNecessary(ViewHolder viewHolder) {
+ if (mActionState == ACTION_STATE_DRAG) {
+ return 0;
+ }
+ final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder);
+ final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection(
+ originalMovementFlags,
+ mRecyclerView.getLayoutDirection());
+ final int flags = (absoluteMovementFlags
+ & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
+ if (flags == 0) {
+ return 0;
+ }
+ final int originalFlags = (originalMovementFlags
+ & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
+ int swipeDir;
+ if (Math.abs(mDx) > Math.abs(mDy)) {
+ if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
+ // if swipe dir is not in original flags, it should be the relative direction
+ if ((originalFlags & swipeDir) == 0) {
+ // convert to relative
+ return Callback.convertToRelativeDirection(swipeDir,
+ mRecyclerView.getLayoutDirection());
+ }
+ return swipeDir;
+ }
+ if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) {
+ return swipeDir;
+ }
+ } else {
+ if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) {
+ return swipeDir;
+ }
+ if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
+ // if swipe dir is not in original flags, it should be the relative direction
+ if ((originalFlags & swipeDir) == 0) {
+ // convert to relative
+ return Callback.convertToRelativeDirection(swipeDir,
+ mRecyclerView.getLayoutDirection());
+ }
+ return swipeDir;
+ }
+ }
+ return 0;
+ }
+
+ private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {
+ if ((flags & (LEFT | RIGHT)) != 0) {
+ final int dirFlag = mDx > 0 ? RIGHT : LEFT;
+ if (mVelocityTracker != null && mActivePointerId > -1) {
+ mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
+ mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
+ final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
+ final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
+ final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT;
+ final float absXVelocity = Math.abs(xVelocity);
+ if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag
+ && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)
+ && absXVelocity > Math.abs(yVelocity)) {
+ return velDirFlag;
+ }
+ }
+
+ final float threshold = mRecyclerView.getWidth() * mCallback
+ .getSwipeThreshold(viewHolder);
+
+ if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) {
+ return dirFlag;
+ }
+ }
+ return 0;
+ }
+
+ private int checkVerticalSwipe(ViewHolder viewHolder, int flags) {
+ if ((flags & (UP | DOWN)) != 0) {
+ final int dirFlag = mDy > 0 ? DOWN : UP;
+ if (mVelocityTracker != null && mActivePointerId > -1) {
+ mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
+ mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
+ final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
+ final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
+ final int velDirFlag = yVelocity > 0f ? DOWN : UP;
+ final float absYVelocity = Math.abs(yVelocity);
+ if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag
+ && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)
+ && absYVelocity > Math.abs(xVelocity)) {
+ return velDirFlag;
+ }
+ }
+
+ final float threshold = mRecyclerView.getHeight() * mCallback
+ .getSwipeThreshold(viewHolder);
+ if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) {
+ return dirFlag;
+ }
+ }
+ return 0;
+ }
+
+ private void addChildDrawingOrderCallback() {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return; // we use elevation on Lollipop
+ }
+ if (mChildDrawingOrderCallback == null) {
+ mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
+ @Override
+ public int onGetChildDrawingOrder(int childCount, int i) {
+ if (mOverdrawChild == null) {
+ return i;
+ }
+ int childPosition = mOverdrawChildPosition;
+ if (childPosition == -1) {
+ childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
+ mOverdrawChildPosition = childPosition;
+ }
+ if (i == childCount - 1) {
+ return childPosition;
+ }
+ return i < childPosition ? i : i + 1;
+ }
+ };
+ }
+ mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
+ }
+
+ void removeChildDrawingOrderCallbackIfNecessary(View view) {
+ if (view == mOverdrawChild) {
+ mOverdrawChild = null;
+ // only remove if we've added
+ if (mChildDrawingOrderCallback != null) {
+ mRecyclerView.setChildDrawingOrderCallback(null);
+ }
+ }
+ }
+
+ /**
+ * An interface which can be implemented by LayoutManager for better integration with
+ * {@link ItemTouchHelper}.
+ */
+ public interface ViewDropHandler {
+
+ /**
+ * Called by the {@link ItemTouchHelper} after a View is dropped over another View.
+ * <p>
+ * A LayoutManager should implement this interface to get ready for the upcoming move
+ * operation.
+ * <p>
+ * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that
+ * the View under drag will be used as an anchor View while calculating the next layout,
+ * making layout stay consistent.
+ *
+ * @param view The View which is being dragged. It is very likely that user is still
+ * dragging this View so there might be other
+ * {@link #prepareForDrop(View, View, int, int)} after this one.
+ * @param target The target view which is being dropped on.
+ * @param x The <code>left</code> offset of the View that is being dragged. This value
+ * includes the movement caused by the user.
+ * @param y The <code>top</code> offset of the View that is being dragged. This value
+ * includes the movement caused by the user.
+ */
+ void prepareForDrop(View view, View target, int x, int y);
+ }
+
+ /**
+ * This class is the contract between ItemTouchHelper and your application. It lets you control
+ * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user
+ * performs these actions.
+ * <p>
+ * To control which actions user can take on each view, you should override
+ * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set
+ * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END},
+ * {@link #UP}, {@link #DOWN}). You can use
+ * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use
+ * {@link SimpleCallback}.
+ * <p>
+ * If user drags an item, ItemTouchHelper will call
+ * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)
+ * onMove(recyclerView, dragged, target)}.
+ * Upon receiving this callback, you should move the item from the old position
+ * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()})
+ * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}.
+ * To control where a View can be dropped, you can override
+ * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a
+ * dragging View overlaps multiple other views, Callback chooses the closest View with which
+ * dragged View might have changed positions. Although this approach works for many use cases,
+ * if you have a custom LayoutManager, you can override
+ * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a
+ * custom drop target.
+ * <p>
+ * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls
+ * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your
+ * adapter (e.g. remove the item) and call related Adapter#notify event.
+ */
+ @SuppressWarnings("UnusedParameters")
+ public abstract static class Callback {
+
+ public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200;
+
+ public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250;
+
+ static final int RELATIVE_DIR_FLAGS = START | END
+ | ((START | END) << DIRECTION_FLAG_COUNT)
+ | ((START | END) << (2 * DIRECTION_FLAG_COUNT));
+
+ private static final ItemTouchUIUtil sUICallback = new ItemTouchUIUtilImpl();
+
+ private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT
+ | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT)
+ | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT));
+
+ private static final Interpolator sDragScrollInterpolator = new Interpolator() {
+ @Override
+ public float getInterpolation(float t) {
+ return t * t * t * t * t;
+ }
+ };
+
+ private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() {
+ @Override
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ /**
+ * Drag scroll speed keeps accelerating until this many milliseconds before being capped.
+ */
+ private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
+
+ private int mCachedMaxScrollSpeed = -1;
+
+ /**
+ * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for
+ * visual
+ * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different
+ * implementations for different platform versions.
+ * <p>
+ * By default, {@link Callback} applies these changes on
+ * {@link RecyclerView.ViewHolder#itemView}.
+ * <p>
+ * For example, if you have a use case where you only want the text to move when user
+ * swipes over the view, you can do the following:
+ * <pre>
+ * public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){
+ * getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView);
+ * }
+ * public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
+ * if (viewHolder != null){
+ * getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView);
+ * }
+ * }
+ * public void onChildDraw(Canvas c, RecyclerView recyclerView,
+ * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
+ * boolean isCurrentlyActive) {
+ * getDefaultUIUtil().onDraw(c, recyclerView,
+ * ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
+ * actionState, isCurrentlyActive);
+ * return true;
+ * }
+ * public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
+ * RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
+ * boolean isCurrentlyActive) {
+ * getDefaultUIUtil().onDrawOver(c, recyclerView,
+ * ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
+ * actionState, isCurrentlyActive);
+ * return true;
+ * }
+ * </pre>
+ *
+ * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback}
+ */
+ public static ItemTouchUIUtil getDefaultUIUtil() {
+ return sUICallback;
+ }
+
+ /**
+ * Replaces a movement direction with its relative version by taking layout direction into
+ * account.
+ *
+ * @param flags The flag value that include any number of movement flags.
+ * @param layoutDirection The layout direction of the View. Can be obtained from
+ * {@link View#getLayoutDirection()}.
+ * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead
+ * of {@link #LEFT}, {@link #RIGHT}.
+ * @see #convertToAbsoluteDirection(int, int)
+ */
+ public static int convertToRelativeDirection(int flags, int layoutDirection) {
+ int masked = flags & ABS_HORIZONTAL_DIR_FLAGS;
+ if (masked == 0) {
+ return flags; // does not have any abs flags, good.
+ }
+ flags &= ~masked; //remove left / right.
+ if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ // no change. just OR with 2 bits shifted mask and return
+ flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT.
+ return flags;
+ } else {
+ // add RIGHT flag as START
+ flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS);
+ // first clean RIGHT bit then add LEFT flag as END
+ flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2;
+ }
+ return flags;
+ }
+
+ /**
+ * Convenience method to create movement flags.
+ * <p>
+ * For instance, if you want to let your items be drag & dropped vertically and swiped
+ * left to be dismissed, you can call this method with:
+ * <code>makeMovementFlags(UP | DOWN, LEFT);</code>
+ *
+ * @param dragFlags The directions in which the item can be dragged.
+ * @param swipeFlags The directions in which the item can be swiped.
+ * @return Returns an integer composed of the given drag and swipe flags.
+ */
+ public static int makeMovementFlags(int dragFlags, int swipeFlags) {
+ return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
+ | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
+ | makeFlag(ACTION_STATE_DRAG, dragFlags);
+ }
+
+ /**
+ * Shifts the given direction flags to the offset of the given action state.
+ *
+ * @param actionState The action state you want to get flags in. Should be one of
+ * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or
+ * {@link #ACTION_STATE_DRAG}.
+ * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN},
+ * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}.
+ * @return And integer that represents the given directions in the provided actionState.
+ */
+ public static int makeFlag(int actionState, int directions) {
+ return directions << (actionState * DIRECTION_FLAG_COUNT);
+ }
+
+ /**
+ * Should return a composite flag which defines the enabled move directions in each state
+ * (idle, swiping, dragging).
+ * <p>
+ * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int,
+ * int)}
+ * or {@link #makeFlag(int, int)}.
+ * <p>
+ * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next
+ * 8 bits are for SWIPE state and third 8 bits are for DRAG state.
+ * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in
+ * {@link ItemTouchHelper}.
+ * <p>
+ * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to
+ * swipe by swiping RIGHT, you can return:
+ * <pre>
+ * makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT);
+ * </pre>
+ * This means, allow right movement while IDLE and allow right and left movement while
+ * swiping.
+ *
+ * @param recyclerView The RecyclerView to which ItemTouchHelper is attached.
+ * @param viewHolder The ViewHolder for which the movement information is necessary.
+ * @return flags specifying which movements are allowed on this ViewHolder.
+ * @see #makeMovementFlags(int, int)
+ * @see #makeFlag(int, int)
+ */
+ public abstract int getMovementFlags(RecyclerView recyclerView,
+ ViewHolder viewHolder);
+
+ /**
+ * Converts a given set of flags to absolution direction which means {@link #START} and
+ * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout
+ * direction.
+ *
+ * @param flags The flag value that include any number of movement flags.
+ * @param layoutDirection The layout direction of the RecyclerView.
+ * @return Updated flags which includes only absolute direction values.
+ */
+ public int convertToAbsoluteDirection(int flags, int layoutDirection) {
+ int masked = flags & RELATIVE_DIR_FLAGS;
+ if (masked == 0) {
+ return flags; // does not have any relative flags, good.
+ }
+ flags &= ~masked; //remove start / end
+ if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ // no change. just OR with 2 bits shifted mask and return
+ flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT.
+ return flags;
+ } else {
+ // add START flag as RIGHT
+ flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS);
+ // first clean start bit then add END flag as LEFT
+ flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2;
+ }
+ return flags;
+ }
+
+ final int getAbsoluteMovementFlags(RecyclerView recyclerView,
+ ViewHolder viewHolder) {
+ final int flags = getMovementFlags(recyclerView, viewHolder);
+ return convertToAbsoluteDirection(flags, recyclerView.getLayoutDirection());
+ }
+
+ boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) {
+ final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
+ return (flags & ACTION_MODE_DRAG_MASK) != 0;
+ }
+
+ boolean hasSwipeFlag(RecyclerView recyclerView,
+ ViewHolder viewHolder) {
+ final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
+ return (flags & ACTION_MODE_SWIPE_MASK) != 0;
+ }
+
+ /**
+ * Return true if the current ViewHolder can be dropped over the the target ViewHolder.
+ * <p>
+ * This method is used when selecting drop target for the dragged View. After Views are
+ * eliminated either via bounds check or via this method, resulting set of views will be
+ * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}.
+ * <p>
+ * Default implementation returns true.
+ *
+ * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
+ * @param current The ViewHolder that user is dragging.
+ * @param target The ViewHolder which is below the dragged ViewHolder.
+ * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false
+ * otherwise.
+ */
+ public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
+ ViewHolder target) {
+ return true;
+ }
+
+ /**
+ * Called when ItemTouchHelper wants to move the dragged item from its old position to
+ * the new position.
+ * <p>
+ * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved
+ * to the adapter position of {@code target} ViewHolder
+ * ({@link ViewHolder#getAdapterPosition()
+ * ViewHolder#getAdapterPosition()}).
+ * <p>
+ * If you don't support drag & drop, this method will never be called.
+ *
+ * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
+ * @param viewHolder The ViewHolder which is being dragged by the user.
+ * @param target The ViewHolder over which the currently active item is being
+ * dragged.
+ * @return True if the {@code viewHolder} has been moved to the adapter position of
+ * {@code target}.
+ * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int)
+ */
+ public abstract boolean onMove(RecyclerView recyclerView,
+ ViewHolder viewHolder, ViewHolder target);
+
+ /**
+ * Returns whether ItemTouchHelper should start a drag and drop operation if an item is
+ * long pressed.
+ * <p>
+ * Default value returns true but you may want to disable this if you want to start
+ * dragging on a custom view touch using {@link #startDrag(ViewHolder)}.
+ *
+ * @return True if ItemTouchHelper should start dragging an item when it is long pressed,
+ * false otherwise. Default value is <code>true</code>.
+ * @see #startDrag(ViewHolder)
+ */
+ public boolean isLongPressDragEnabled() {
+ return true;
+ }
+
+ /**
+ * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped
+ * over the View.
+ * <p>
+ * Default value returns true but you may want to disable this if you want to start
+ * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}.
+ *
+ * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer
+ * over the View, false otherwise. Default value is <code>true</code>.
+ * @see #startSwipe(ViewHolder)
+ */
+ public boolean isItemViewSwipeEnabled() {
+ return true;
+ }
+
+ /**
+ * When finding views under a dragged view, by default, ItemTouchHelper searches for views
+ * that overlap with the dragged View. By overriding this method, you can extend or shrink
+ * the search box.
+ *
+ * @return The extra margin to be added to the hit box of the dragged View.
+ */
+ public int getBoundingBoxMargin() {
+ return 0;
+ }
+
+ /**
+ * Returns the fraction that the user should move the View to be considered as swiped.
+ * The fraction is calculated with respect to RecyclerView's bounds.
+ * <p>
+ * Default value is .5f, which means, to swipe a View, user must move the View at least
+ * half of RecyclerView's width or height, depending on the swipe direction.
+ *
+ * @param viewHolder The ViewHolder that is being dragged.
+ * @return A float value that denotes the fraction of the View size. Default value
+ * is .5f .
+ */
+ public float getSwipeThreshold(ViewHolder viewHolder) {
+ return .5f;
+ }
+
+ /**
+ * Returns the fraction that the user should move the View to be considered as it is
+ * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views
+ * below it for a possible drop.
+ *
+ * @param viewHolder The ViewHolder that is being dragged.
+ * @return A float value that denotes the fraction of the View size. Default value is
+ * .5f .
+ */
+ public float getMoveThreshold(ViewHolder viewHolder) {
+ return .5f;
+ }
+
+ /**
+ * Defines the minimum velocity which will be considered as a swipe action by the user.
+ * <p>
+ * You can increase this value to make it harder to swipe or decrease it to make it easier.
+ * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure
+ * current direction velocity is larger then the perpendicular one. Otherwise, user's
+ * movement is ambiguous. You can change the threshold by overriding
+ * {@link #getSwipeVelocityThreshold(float)}.
+ * <p>
+ * The velocity is calculated in pixels per second.
+ * <p>
+ * The default framework value is passed as a parameter so that you can modify it with a
+ * multiplier.
+ *
+ * @param defaultValue The default value (in pixels per second) used by the
+ * ItemTouchHelper.
+ * @return The minimum swipe velocity. The default implementation returns the
+ * <code>defaultValue</code> parameter.
+ * @see #getSwipeVelocityThreshold(float)
+ * @see #getSwipeThreshold(ViewHolder)
+ */
+ public float getSwipeEscapeVelocity(float defaultValue) {
+ return defaultValue;
+ }
+
+ /**
+ * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements.
+ * <p>
+ * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the
+ * perpendicular movement. If both directions reach to the max threshold, none of them will
+ * be considered as a swipe because it is usually an indication that user rather tried to
+ * scroll then swipe.
+ * <p>
+ * The velocity is calculated in pixels per second.
+ * <p>
+ * You can customize this behavior by changing this method. If you increase the value, it
+ * will be easier for the user to swipe diagonally and if you decrease the value, user will
+ * need to make a rather straight finger movement to trigger a swipe.
+ *
+ * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper.
+ * @return The velocity cap for pointer movements. The default implementation returns the
+ * <code>defaultValue</code> parameter.
+ * @see #getSwipeEscapeVelocity(float)
+ */
+ public float getSwipeVelocityThreshold(float defaultValue) {
+ return defaultValue;
+ }
+
+ /**
+ * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that
+ * are under the dragged View.
+ * <p>
+ * Default implementation filters the View with which dragged item have changed position
+ * in the drag direction. For instance, if the view is dragged UP, it compares the
+ * <code>view.getTop()</code> of the two views before and after drag started. If that value
+ * is different, the target view passes the filter.
+ * <p>
+ * Among these Views which pass the test, the one closest to the dragged view is chosen.
+ * <p>
+ * This method is called on the main thread every time user moves the View. If you want to
+ * override it, make sure it does not do any expensive operations.
+ *
+ * @param selected The ViewHolder being dragged by the user.
+ * @param dropTargets The list of ViewHolder that are under the dragged View and
+ * candidate as a drop.
+ * @param curX The updated left value of the dragged View after drag translations
+ * are applied. This value does not include margins added by
+ * {@link RecyclerView.ItemDecoration}s.
+ * @param curY The updated top value of the dragged View after drag translations
+ * are applied. This value does not include margins added by
+ * {@link RecyclerView.ItemDecoration}s.
+ * @return A ViewHolder to whose position the dragged ViewHolder should be
+ * moved to.
+ */
+ public ViewHolder chooseDropTarget(ViewHolder selected,
+ List<ViewHolder> dropTargets, int curX, int curY) {
+ int right = curX + selected.itemView.getWidth();
+ int bottom = curY + selected.itemView.getHeight();
+ ViewHolder winner = null;
+ int winnerScore = -1;
+ final int dx = curX - selected.itemView.getLeft();
+ final int dy = curY - selected.itemView.getTop();
+ final int targetsSize = dropTargets.size();
+ for (int i = 0; i < targetsSize; i++) {
+ final ViewHolder target = dropTargets.get(i);
+ if (dx > 0) {
+ int diff = target.itemView.getRight() - right;
+ if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) {
+ final int score = Math.abs(diff);
+ if (score > winnerScore) {
+ winnerScore = score;
+ winner = target;
+ }
+ }
+ }
+ if (dx < 0) {
+ int diff = target.itemView.getLeft() - curX;
+ if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) {
+ final int score = Math.abs(diff);
+ if (score > winnerScore) {
+ winnerScore = score;
+ winner = target;
+ }
+ }
+ }
+ if (dy < 0) {
+ int diff = target.itemView.getTop() - curY;
+ if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) {
+ final int score = Math.abs(diff);
+ if (score > winnerScore) {
+ winnerScore = score;
+ winner = target;
+ }
+ }
+ }
+
+ if (dy > 0) {
+ int diff = target.itemView.getBottom() - bottom;
+ if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) {
+ final int score = Math.abs(diff);
+ if (score > winnerScore) {
+ winnerScore = score;
+ winner = target;
+ }
+ }
+ }
+ }
+ return winner;
+ }
+
+ /**
+ * Called when a ViewHolder is swiped by the user.
+ * <p>
+ * If you are returning relative directions ({@link #START} , {@link #END}) from the
+ * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method
+ * will also use relative directions. Otherwise, it will use absolute directions.
+ * <p>
+ * If you don't support swiping, this method will never be called.
+ * <p>
+ * ItemTouchHelper will keep a reference to the View until it is detached from
+ * RecyclerView.
+ * As soon as it is detached, ItemTouchHelper will call
+ * {@link #clearView(RecyclerView, ViewHolder)}.
+ *
+ * @param viewHolder The ViewHolder which has been swiped by the user.
+ * @param direction The direction to which the ViewHolder is swiped. It is one of
+ * {@link #UP}, {@link #DOWN},
+ * {@link #LEFT} or {@link #RIGHT}. If your
+ * {@link #getMovementFlags(RecyclerView, ViewHolder)}
+ * method
+ * returned relative flags instead of {@link #LEFT} / {@link #RIGHT};
+ * `direction` will be relative as well. ({@link #START} or {@link
+ * #END}).
+ */
+ public abstract void onSwiped(ViewHolder viewHolder, int direction);
+
+ /**
+ * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed.
+ * <p/>
+ * If you override this method, you should call super.
+ *
+ * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if
+ * it is cleared.
+ * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE},
+ * {@link ItemTouchHelper#ACTION_STATE_SWIPE} or
+ * {@link ItemTouchHelper#ACTION_STATE_DRAG}.
+ * @see #clearView(RecyclerView, RecyclerView.ViewHolder)
+ */
+ public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
+ if (viewHolder != null) {
+ sUICallback.onSelected(viewHolder.itemView);
+ }
+ }
+
+ private int getMaxDragScroll(RecyclerView recyclerView) {
+ if (mCachedMaxScrollSpeed == -1) {
+ mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize(
+ R.dimen.item_touch_helper_max_drag_scroll_per_frame);
+ }
+ return mCachedMaxScrollSpeed;
+ }
+
+ /**
+ * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true.
+ * <p>
+ * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it
+ * modifies the existing View. Because of this reason, it is important that the View is
+ * still part of the layout after it is moved. This may not work as intended when swapped
+ * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views
+ * which were not eligible for dropping over).
+ * <p>
+ * This method is responsible to give necessary hint to the LayoutManager so that it will
+ * keep the View in visible area. For example, for LinearLayoutManager, this is as simple
+ * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}.
+ *
+ * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's
+ * new position is likely to be out of bounds.
+ * <p>
+ * It is important to ensure the ViewHolder will stay visible as otherwise, it might be
+ * removed by the LayoutManager if the move causes the View to go out of bounds. In that
+ * case, drag will end prematurely.
+ *
+ * @param recyclerView The RecyclerView controlled by the ItemTouchHelper.
+ * @param viewHolder The ViewHolder under user's control.
+ * @param fromPos The previous adapter position of the dragged item (before it was
+ * moved).
+ * @param target The ViewHolder on which the currently active item has been dropped.
+ * @param toPos The new adapter position of the dragged item.
+ * @param x The updated left value of the dragged View after drag translations
+ * are applied. This value does not include margins added by
+ * {@link RecyclerView.ItemDecoration}s.
+ * @param y The updated top value of the dragged View after drag translations
+ * are applied. This value does not include margins added by
+ * {@link RecyclerView.ItemDecoration}s.
+ */
+ public void onMoved(final RecyclerView recyclerView,
+ final ViewHolder viewHolder, int fromPos, final ViewHolder target, int toPos, int x,
+ int y) {
+ final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+ if (layoutManager instanceof ViewDropHandler) {
+ ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView,
+ target.itemView, x, y);
+ return;
+ }
+
+ // if layout manager cannot handle it, do some guesswork
+ if (layoutManager.canScrollHorizontally()) {
+ final int minLeft = layoutManager.getDecoratedLeft(target.itemView);
+ if (minLeft <= recyclerView.getPaddingLeft()) {
+ recyclerView.scrollToPosition(toPos);
+ }
+ final int maxRight = layoutManager.getDecoratedRight(target.itemView);
+ if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) {
+ recyclerView.scrollToPosition(toPos);
+ }
+ }
+
+ if (layoutManager.canScrollVertically()) {
+ final int minTop = layoutManager.getDecoratedTop(target.itemView);
+ if (minTop <= recyclerView.getPaddingTop()) {
+ recyclerView.scrollToPosition(toPos);
+ }
+ final int maxBottom = layoutManager.getDecoratedBottom(target.itemView);
+ if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) {
+ recyclerView.scrollToPosition(toPos);
+ }
+ }
+ }
+
+ void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
+ List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
+ int actionState, float dX, float dY) {
+ final int recoverAnimSize = recoverAnimationList.size();
+ for (int i = 0; i < recoverAnimSize; i++) {
+ final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
+ anim.update();
+ final int count = c.save();
+ onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
+ false);
+ c.restoreToCount(count);
+ }
+ if (selected != null) {
+ final int count = c.save();
+ onChildDraw(c, parent, selected, dX, dY, actionState, true);
+ c.restoreToCount(count);
+ }
+ }
+
+ void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected,
+ List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
+ int actionState, float dX, float dY) {
+ final int recoverAnimSize = recoverAnimationList.size();
+ for (int i = 0; i < recoverAnimSize; i++) {
+ final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
+ final int count = c.save();
+ onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
+ false);
+ c.restoreToCount(count);
+ }
+ if (selected != null) {
+ final int count = c.save();
+ onChildDrawOver(c, parent, selected, dX, dY, actionState, true);
+ c.restoreToCount(count);
+ }
+ boolean hasRunningAnimation = false;
+ for (int i = recoverAnimSize - 1; i >= 0; i--) {
+ final RecoverAnimation anim = recoverAnimationList.get(i);
+ if (anim.mEnded && !anim.mIsPendingCleanup) {
+ recoverAnimationList.remove(i);
+ } else if (!anim.mEnded) {
+ hasRunningAnimation = true;
+ }
+ }
+ if (hasRunningAnimation) {
+ parent.invalidate();
+ }
+ }
+
+ /**
+ * Called by the ItemTouchHelper when the user interaction with an element is over and it
+ * also completed its animation.
+ * <p>
+ * This is a good place to clear all changes on the View that was done in
+ * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)},
+ * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
+ * boolean)} or
+ * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}.
+ *
+ * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper.
+ * @param viewHolder The View that was interacted by the user.
+ */
+ public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
+ sUICallback.clearView(viewHolder.itemView);
+ }
+
+ /**
+ * Called by ItemTouchHelper on RecyclerView's onDraw callback.
+ * <p>
+ * If you would like to customize how your View's respond to user interactions, this is
+ * a good place to override.
+ * <p>
+ * Default implementation translates the child by the given <code>dX</code>,
+ * <code>dY</code>.
+ * ItemTouchHelper also takes care of drawing the child after other children if it is being
+ * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this
+ * is
+ * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L
+ * and after, it changes View's elevation value to be greater than all other children.)
+ *
+ * @param c The canvas which RecyclerView is drawing its children
+ * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to
+ * @param viewHolder The ViewHolder which is being interacted by the User or it was
+ * interacted and simply animating to its original position
+ * @param dX The amount of horizontal displacement caused by user's action
+ * @param dY The amount of vertical displacement caused by user's action
+ * @param actionState The type of interaction on the View. Is either {@link
+ * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
+ * @param isCurrentlyActive True if this view is currently being controlled by the user or
+ * false it is simply animating back to its original state.
+ * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
+ * boolean)
+ */
+ public void onChildDraw(Canvas c, RecyclerView recyclerView,
+ ViewHolder viewHolder,
+ float dX, float dY, int actionState, boolean isCurrentlyActive) {
+ sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
+ isCurrentlyActive);
+ }
+
+ /**
+ * Called by ItemTouchHelper on RecyclerView's onDraw callback.
+ * <p>
+ * If you would like to customize how your View's respond to user interactions, this is
+ * a good place to override.
+ * <p>
+ * Default implementation translates the child by the given <code>dX</code>,
+ * <code>dY</code>.
+ * ItemTouchHelper also takes care of drawing the child after other children if it is being
+ * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this
+ * is
+ * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L
+ * and after, it changes View's elevation value to be greater than all other children.)
+ *
+ * @param c The canvas which RecyclerView is drawing its children
+ * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to
+ * @param viewHolder The ViewHolder which is being interacted by the User or it was
+ * interacted and simply animating to its original position
+ * @param dX The amount of horizontal displacement caused by user's action
+ * @param dY The amount of vertical displacement caused by user's action
+ * @param actionState The type of interaction on the View. Is either {@link
+ * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
+ * @param isCurrentlyActive True if this view is currently being controlled by the user or
+ * false it is simply animating back to its original state.
+ * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
+ * boolean)
+ */
+ public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
+ ViewHolder viewHolder,
+ float dX, float dY, int actionState, boolean isCurrentlyActive) {
+ sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
+ isCurrentlyActive);
+ }
+
+ /**
+ * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View
+ * will be animated to its final position.
+ * <p>
+ * Default implementation uses ItemAnimator's duration values. If
+ * <code>animationType</code> is {@link #ANIMATION_TYPE_DRAG}, it returns
+ * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns
+ * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have
+ * any {@link RecyclerView.ItemAnimator} attached, this method returns
+ * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION}
+ * depending on the animation type.
+ *
+ * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to.
+ * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG},
+ * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or
+ * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}.
+ * @param animateDx The horizontal distance that the animation will offset
+ * @param animateDy The vertical distance that the animation will offset
+ * @return The duration for the animation
+ */
+ public long getAnimationDuration(RecyclerView recyclerView, int animationType,
+ float animateDx, float animateDy) {
+ final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator();
+ if (itemAnimator == null) {
+ return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION
+ : DEFAULT_SWIPE_ANIMATION_DURATION;
+ } else {
+ return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration()
+ : itemAnimator.getRemoveDuration();
+ }
+ }
+
+ /**
+ * Called by the ItemTouchHelper when user is dragging a view out of bounds.
+ * <p>
+ * You can override this method to decide how much RecyclerView should scroll in response
+ * to this action. Default implementation calculates a value based on the amount of View
+ * out of bounds and the time it spent there. The longer user keeps the View out of bounds,
+ * the faster the list will scroll. Similarly, the larger portion of the View is out of
+ * bounds, the faster the RecyclerView will scroll.
+ *
+ * @param recyclerView The RecyclerView instance to which ItemTouchHelper is
+ * attached to.
+ * @param viewSize The total size of the View in scroll direction, excluding
+ * item decorations.
+ * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value
+ * is negative if the View is dragged towards left or top edge.
+ * @param totalSize The total size of RecyclerView in the scroll direction.
+ * @param msSinceStartScroll The time passed since View is kept out of bounds.
+ * @return The amount that RecyclerView should scroll. Keep in mind that this value will
+ * be passed to {@link RecyclerView#scrollBy(int, int)} method.
+ */
+ public int interpolateOutOfBoundsScroll(RecyclerView recyclerView,
+ int viewSize, int viewSizeOutOfBounds,
+ int totalSize, long msSinceStartScroll) {
+ final int maxScroll = getMaxDragScroll(recyclerView);
+ final int absOutOfBounds = Math.abs(viewSizeOutOfBounds);
+ final int direction = (int) Math.signum(viewSizeOutOfBounds);
+ // might be negative if other direction
+ float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize);
+ final int cappedScroll = (int) (direction * maxScroll
+ * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio));
+ final float timeRatio;
+ if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) {
+ timeRatio = 1f;
+ } else {
+ timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS;
+ }
+ final int value = (int) (cappedScroll * sDragScrollInterpolator
+ .getInterpolation(timeRatio));
+ if (value == 0) {
+ return viewSizeOutOfBounds > 0 ? 1 : -1;
+ }
+ return value;
+ }
+ }
+
+ /**
+ * A simple wrapper to the default Callback which you can construct with drag and swipe
+ * directions and this class will handle the flag callbacks. You should still override onMove
+ * or
+ * onSwiped depending on your use case.
+ *
+ * <pre>
+ * ItemTouchHelper mIth = new ItemTouchHelper(
+ * new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
+ * ItemTouchHelper.LEFT) {
+ * public abstract boolean onMove(RecyclerView recyclerView,
+ * ViewHolder viewHolder, ViewHolder target) {
+ * final int fromPos = viewHolder.getAdapterPosition();
+ * final int toPos = target.getAdapterPosition();
+ * // move item in `fromPos` to `toPos` in adapter.
+ * return true;// true if moved, false otherwise
+ * }
+ * public void onSwiped(ViewHolder viewHolder, int direction) {
+ * // remove from adapter
+ * }
+ * });
+ * </pre>
+ */
+ public abstract static class SimpleCallback extends Callback {
+
+ private int mDefaultSwipeDirs;
+
+ private int mDefaultDragDirs;
+
+ /**
+ * Creates a Callback for the given drag and swipe allowance. These values serve as
+ * defaults
+ * and if you want to customize behavior per ViewHolder, you can override
+ * {@link #getSwipeDirs(RecyclerView, ViewHolder)}
+ * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}.
+ *
+ * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be
+ * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link
+ * #END},
+ * {@link #UP} and {@link #DOWN}.
+ * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be
+ * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link
+ * #END},
+ * {@link #UP} and {@link #DOWN}.
+ */
+ public SimpleCallback(int dragDirs, int swipeDirs) {
+ mDefaultSwipeDirs = swipeDirs;
+ mDefaultDragDirs = dragDirs;
+ }
+
+ /**
+ * Updates the default swipe directions. For example, you can use this method to toggle
+ * certain directions depending on your use case.
+ *
+ * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped.
+ */
+ public void setDefaultSwipeDirs(int defaultSwipeDirs) {
+ mDefaultSwipeDirs = defaultSwipeDirs;
+ }
+
+ /**
+ * Updates the default drag directions. For example, you can use this method to toggle
+ * certain directions depending on your use case.
+ *
+ * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged.
+ */
+ public void setDefaultDragDirs(int defaultDragDirs) {
+ mDefaultDragDirs = defaultDragDirs;
+ }
+
+ /**
+ * Returns the swipe directions for the provided ViewHolder.
+ * Default implementation returns the swipe directions that was set via constructor or
+ * {@link #setDefaultSwipeDirs(int)}.
+ *
+ * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to.
+ * @param viewHolder The RecyclerView for which the swipe direction is queried.
+ * @return A binary OR of direction flags.
+ */
+ public int getSwipeDirs(RecyclerView recyclerView, ViewHolder viewHolder) {
+ return mDefaultSwipeDirs;
+ }
+
+ /**
+ * Returns the drag directions for the provided ViewHolder.
+ * Default implementation returns the drag directions that was set via constructor or
+ * {@link #setDefaultDragDirs(int)}.
+ *
+ * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to.
+ * @param viewHolder The RecyclerView for which the swipe direction is queried.
+ * @return A binary OR of direction flags.
+ */
+ public int getDragDirs(RecyclerView recyclerView, ViewHolder viewHolder) {
+ return mDefaultDragDirs;
+ }
+
+ @Override
+ public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
+ return makeMovementFlags(getDragDirs(recyclerView, viewHolder),
+ getSwipeDirs(recyclerView, viewHolder));
+ }
+ }
+
+ private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
+
+ ItemTouchHelperGestureListener() {
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ View child = findChildView(e);
+ if (child != null) {
+ ViewHolder vh = mRecyclerView.getChildViewHolder(child);
+ if (vh != null) {
+ if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
+ return;
+ }
+ int pointerId = e.getPointerId(0);
+ // Long press is deferred.
+ // Check w/ active pointer id to avoid selecting after motion
+ // event is canceled.
+ if (pointerId == mActivePointerId) {
+ final int index = e.findPointerIndex(mActivePointerId);
+ final float x = e.getX(index);
+ final float y = e.getY(index);
+ mInitialTouchX = x;
+ mInitialTouchY = y;
+ mDx = mDy = 0f;
+ if (DEBUG) {
+ Log.d(TAG,
+ "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
+ }
+ if (mCallback.isLongPressDragEnabled()) {
+ select(vh, ACTION_STATE_DRAG);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private class RecoverAnimation implements Animator.AnimatorListener {
+
+ final float mStartDx;
+
+ final float mStartDy;
+
+ final float mTargetX;
+
+ final float mTargetY;
+
+ final ViewHolder mViewHolder;
+
+ final int mActionState;
+
+ private final ValueAnimator mValueAnimator;
+
+ final int mAnimationType;
+
+ public boolean mIsPendingCleanup;
+
+ float mX;
+
+ float mY;
+
+ // if user starts touching a recovering view, we put it into interaction mode again,
+ // instantly.
+ boolean mOverridden = false;
+
+ boolean mEnded = false;
+
+ private float mFraction;
+
+ RecoverAnimation(ViewHolder viewHolder, int animationType,
+ int actionState, float startDx, float startDy, float targetX, float targetY) {
+ mActionState = actionState;
+ mAnimationType = animationType;
+ mViewHolder = viewHolder;
+ mStartDx = startDx;
+ mStartDy = startDy;
+ mTargetX = targetX;
+ mTargetY = targetY;
+ mValueAnimator = ValueAnimator.ofFloat(0f, 1f);
+ mValueAnimator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setFraction(animation.getAnimatedFraction());
+ }
+ });
+ mValueAnimator.setTarget(viewHolder.itemView);
+ mValueAnimator.addListener(this);
+ setFraction(0f);
+ }
+
+ public void setDuration(long duration) {
+ mValueAnimator.setDuration(duration);
+ }
+
+ public void start() {
+ mViewHolder.setIsRecyclable(false);
+ mValueAnimator.start();
+ }
+
+ public void cancel() {
+ mValueAnimator.cancel();
+ }
+
+ public void setFraction(float fraction) {
+ mFraction = fraction;
+ }
+
+ /**
+ * We run updates on onDraw method but use the fraction from animator callback.
+ * This way, we can sync translate x/y values w/ the animators to avoid one-off frames.
+ */
+ public void update() {
+ if (mStartDx == mTargetX) {
+ mX = mViewHolder.itemView.getTranslationX();
+ } else {
+ mX = mStartDx + mFraction * (mTargetX - mStartDx);
+ }
+ if (mStartDy == mTargetY) {
+ mY = mViewHolder.itemView.getTranslationY();
+ } else {
+ mY = mStartDy + mFraction * (mTargetY - mStartDy);
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mEnded) {
+ mViewHolder.setIsRecyclable(true);
+ }
+ mEnded = true;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ setFraction(1f); //make sure we recover the view's state.
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ }
+}
diff --git a/com/android/internal/widget/helper/ItemTouchUIUtil.java b/com/android/internal/widget/helper/ItemTouchUIUtil.java
new file mode 100644
index 0000000..e368a6d
--- /dev/null
+++ b/com/android/internal/widget/helper/ItemTouchUIUtil.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 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.internal.widget.helper;
+
+import android.graphics.Canvas;
+import android.view.View;
+
+import com.android.internal.widget.RecyclerView;
+
+/**
+ * Utility class for {@link ItemTouchHelper} which handles item transformations for different
+ * API versions.
+ * <p/>
+ * This class has methods that map to {@link ItemTouchHelper.Callback}'s drawing methods. Default
+ * implementations in {@link ItemTouchHelper.Callback} call these methods with
+ * {@link RecyclerView.ViewHolder#itemView} and {@link ItemTouchUIUtil} makes necessary changes
+ * on the View depending on the API level. You can access the instance of {@link ItemTouchUIUtil}
+ * via {@link ItemTouchHelper.Callback#getDefaultUIUtil()} and call its methods with the children
+ * of ViewHolder that you want to apply default effects.
+ *
+ * @see ItemTouchHelper.Callback#getDefaultUIUtil()
+ */
+public interface ItemTouchUIUtil {
+
+ /**
+ * The default implementation for {@link ItemTouchHelper.Callback#onChildDraw(Canvas,
+ * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}
+ */
+ void onDraw(Canvas c, RecyclerView recyclerView, View view,
+ float dX, float dY, int actionState, boolean isCurrentlyActive);
+
+ /**
+ * The default implementation for {@link ItemTouchHelper.Callback#onChildDrawOver(Canvas,
+ * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}
+ */
+ void onDrawOver(Canvas c, RecyclerView recyclerView, View view,
+ float dX, float dY, int actionState, boolean isCurrentlyActive);
+
+ /**
+ * The default implementation for {@link ItemTouchHelper.Callback#clearView(RecyclerView,
+ * RecyclerView.ViewHolder)}
+ */
+ void clearView(View view);
+
+ /**
+ * The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged(
+ * RecyclerView.ViewHolder, int)}
+ */
+ void onSelected(View view);
+}
+
diff --git a/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java b/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java
new file mode 100644
index 0000000..0de240b
--- /dev/null
+++ b/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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.internal.widget.helper;
+
+import android.graphics.Canvas;
+import android.view.View;
+
+import com.android.internal.R;
+import com.android.internal.widget.RecyclerView;
+
+/**
+ * Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them
+ * public API, which is not desired in this case.
+ */
+class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
+ @Override
+ public void onDraw(Canvas c, RecyclerView recyclerView, View view,
+ float dX, float dY, int actionState, boolean isCurrentlyActive) {
+ if (isCurrentlyActive) {
+ Object originalElevation = view.getTag(
+ R.id.item_touch_helper_previous_elevation);
+ if (originalElevation == null) {
+ originalElevation = view.getElevation();
+ float newElevation = 1f + findMaxElevation(recyclerView, view);
+ view.setElevation(newElevation);
+ view.setTag(R.id.item_touch_helper_previous_elevation,
+ originalElevation);
+ }
+ }
+ view.setTranslationX(dX);
+ view.setTranslationY(dY);
+ }
+
+ private float findMaxElevation(RecyclerView recyclerView, View itemView) {
+ final int childCount = recyclerView.getChildCount();
+ float max = 0;
+ for (int i = 0; i < childCount; i++) {
+ final View child = recyclerView.getChildAt(i);
+ if (child == itemView) {
+ continue;
+ }
+ final float elevation = child.getElevation();
+ if (elevation > max) {
+ max = elevation;
+ }
+ }
+ return max;
+ }
+
+ @Override
+ public void clearView(View view) {
+ final Object tag = view.getTag(
+ R.id.item_touch_helper_previous_elevation);
+ if (tag != null && tag instanceof Float) {
+ view.setElevation((Float) tag);
+ }
+ view.setTag(R.id.item_touch_helper_previous_elevation, null);
+ view.setTranslationX(0f);
+ view.setTranslationY(0f);
+ }
+
+ @Override
+ public void onSelected(View view) {
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView recyclerView,
+ View view, float dX, float dY, int actionState, boolean isCurrentlyActive) {
+ }
+}