/* * 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 static com.android.launcher3.anim.Interpolators.LINEAR; 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.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) { setter.setViewAlpha(this, hasContent ? 1 : 0, LINEAR); allowTouchForwarding(hasContent); } protected void allowTouchForwarding(boolean allow) { mAllowTouchForwarding = allow; } public boolean hasVisibleContent() { return false; } @Override public boolean hasOverlappingRendering() { return false; } }