| /* |
| * 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.launcher3.allapps; |
| |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.v7.widget.RecyclerView; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.animation.Interpolator; |
| import android.widget.LinearLayout; |
| |
| import com.android.launcher3.R; |
| import com.android.launcher3.anim.PropertySetter; |
| |
| public class FloatingHeaderView extends LinearLayout implements |
| ValueAnimator.AnimatorUpdateListener { |
| |
| private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE); |
| private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0); |
| private final Point mTempOffset = new Point(); |
| private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
| } |
| |
| @Override |
| public void onScrolled(RecyclerView rv, int dx, int dy) { |
| if (rv != mCurrentRV) { |
| return; |
| } |
| |
| if (mAnimator.isStarted()) { |
| mAnimator.cancel(); |
| } |
| |
| int current = -mCurrentRV.getCurrentScrollY(); |
| moved(current); |
| apply(); |
| } |
| }; |
| |
| protected ViewGroup mTabLayout; |
| private AllAppsRecyclerView mMainRV; |
| private AllAppsRecyclerView mWorkRV; |
| private AllAppsRecyclerView mCurrentRV; |
| private ViewGroup mParent; |
| private boolean mHeaderCollapsed; |
| private int mSnappedScrolledY; |
| private int mTranslationY; |
| |
| private boolean mAllowTouchForwarding; |
| private boolean mForwardToRecyclerView; |
| |
| protected boolean mTabsHidden; |
| protected int mMaxTranslation; |
| private boolean mMainRVActive = true; |
| |
| public FloatingHeaderView(@NonNull Context context) { |
| this(context, null); |
| } |
| |
| public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mTabLayout = findViewById(R.id.tabs); |
| } |
| |
| public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) { |
| mTabsHidden = tabsHidden; |
| mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE); |
| mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView); |
| mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView); |
| mParent = (ViewGroup) mMainRV.getParent(); |
| setMainActive(mMainRVActive || mWorkRV == null); |
| reset(false); |
| } |
| |
| private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) { |
| if (old != updated && updated != null ) { |
| updated.addOnScrollListener(mOnScrollListener); |
| } |
| return updated; |
| } |
| |
| public void setMainActive(boolean active) { |
| mCurrentRV = active ? mMainRV : mWorkRV; |
| mMainRVActive = active; |
| } |
| |
| public int getMaxTranslation() { |
| if (mMaxTranslation == 0 && mTabsHidden) { |
| return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding); |
| } else if (mMaxTranslation > 0 && mTabsHidden) { |
| return mMaxTranslation + getPaddingTop(); |
| } else { |
| return mMaxTranslation; |
| } |
| } |
| |
| private boolean canSnapAt(int currentScrollY) { |
| return Math.abs(currentScrollY) <= mMaxTranslation; |
| } |
| |
| private void moved(final int currentScrollY) { |
| if (mHeaderCollapsed) { |
| if (currentScrollY <= mSnappedScrolledY) { |
| if (canSnapAt(currentScrollY)) { |
| mSnappedScrolledY = currentScrollY; |
| } |
| } else { |
| mHeaderCollapsed = false; |
| } |
| mTranslationY = currentScrollY; |
| } else if (!mHeaderCollapsed) { |
| mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation; |
| |
| // update state vars |
| if (mTranslationY >= 0) { // expanded: must not move down further |
| mTranslationY = 0; |
| mSnappedScrolledY = currentScrollY - mMaxTranslation; |
| } else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden |
| mHeaderCollapsed = true; |
| mSnappedScrolledY = -mMaxTranslation; |
| } |
| } |
| } |
| |
| protected void applyScroll(int uncappedY, int currentY) { } |
| |
| protected void apply() { |
| int uncappedTranslationY = mTranslationY; |
| mTranslationY = Math.max(mTranslationY, -mMaxTranslation); |
| applyScroll(uncappedTranslationY, mTranslationY); |
| mTabLayout.setTranslationY(mTranslationY); |
| mClip.top = mMaxTranslation + mTranslationY; |
| // clipping on a draw might cause additional redraw |
| mMainRV.setClipBounds(mClip); |
| if (mWorkRV != null) { |
| mWorkRV.setClipBounds(mClip); |
| } |
| } |
| |
| public void reset(boolean animate) { |
| if (mAnimator.isStarted()) { |
| mAnimator.cancel(); |
| } |
| if (animate) { |
| mAnimator.setIntValues(mTranslationY, 0); |
| mAnimator.addUpdateListener(this); |
| mAnimator.setDuration(150); |
| mAnimator.start(); |
| } else { |
| mTranslationY = 0; |
| apply(); |
| } |
| mHeaderCollapsed = false; |
| mSnappedScrolledY = -mMaxTranslation; |
| mCurrentRV.scrollToTop(); |
| } |
| |
| public boolean isExpanded() { |
| return !mHeaderCollapsed; |
| } |
| |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| mTranslationY = (Integer) animation.getAnimatedValue(); |
| apply(); |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| if (!mAllowTouchForwarding) { |
| mForwardToRecyclerView = false; |
| return super.onInterceptTouchEvent(ev); |
| } |
| calcOffset(mTempOffset); |
| ev.offsetLocation(mTempOffset.x, mTempOffset.y); |
| mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev); |
| ev.offsetLocation(-mTempOffset.x, -mTempOffset.y); |
| return mForwardToRecyclerView || super.onInterceptTouchEvent(ev); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| if (mForwardToRecyclerView) { |
| // take this view's and parent view's (view pager) location into account |
| calcOffset(mTempOffset); |
| event.offsetLocation(mTempOffset.x, mTempOffset.y); |
| try { |
| return mCurrentRV.onTouchEvent(event); |
| } finally { |
| event.offsetLocation(-mTempOffset.x, -mTempOffset.y); |
| } |
| } else { |
| return super.onTouchEvent(event); |
| } |
| } |
| |
| private void calcOffset(Point p) { |
| p.x = getLeft() - mCurrentRV.getLeft() - mParent.getLeft(); |
| p.y = getTop() - mCurrentRV.getTop() - mParent.getTop(); |
| } |
| |
| public void setContentVisibility(boolean hasHeader, boolean hasContent, PropertySetter setter, |
| Interpolator fadeInterpolator) { |
| setter.setViewAlpha(this, hasContent ? 1 : 0, fadeInterpolator); |
| allowTouchForwarding(hasContent); |
| } |
| |
| protected void allowTouchForwarding(boolean allow) { |
| mAllowTouchForwarding = allow; |
| } |
| |
| public boolean hasVisibleContent() { |
| return false; |
| } |
| |
| @Override |
| public boolean hasOverlappingRendering() { |
| return false; |
| } |
| } |
| |
| |