blob: b4685ea37224af1feceedca891929c312040d71e [file] [log] [blame]
/*
* 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.res.Resources;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import com.android.launcher3.R;
public class FloatingHeaderHandler extends RecyclerView.OnScrollListener
implements ValueAnimator.AnimatorUpdateListener {
private static final boolean SHOW_PREDICTIONS_ONLY_ON_TOP = true;
private final View mHeaderView;
private final PredictionRowView mPredictionRow;
private final ViewGroup mTabLayout;
private final View mDivider;
private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
private AllAppsRecyclerView mMainRV;
private AllAppsRecyclerView mWorkRV;
private boolean mTopOnlyMode;
private boolean mHeaderHidden;
private int mMaxTranslation;
private int mSnappedScrolledY;
private int mTranslationY;
private int mMainScrolledY;
private int mWorkScrolledY;
private boolean mMainRVActive;
public FloatingHeaderHandler(@NonNull ViewGroup header) {
mHeaderView = header;
mTabLayout = header.findViewById(R.id.tabs);
mDivider = header.findViewById(R.id.divider);
mPredictionRow = header.findViewById(R.id.header_content);
}
public void setup(@NonNull AllAppsRecyclerView personalRV, @Nullable AllAppsRecyclerView workRV,
int predictionRowHeight) {
mTopOnlyMode = workRV == null;
mTabLayout.setVisibility(mTopOnlyMode ? View.GONE : View.VISIBLE);
mPredictionRow.getLayoutParams().height = predictionRowHeight;
mMaxTranslation = predictionRowHeight;
mMainRV = setupRV(mMainRV, personalRV);
mWorkRV = setupRV(mWorkRV, workRV);
setMainActive(true);
setupDivider();
}
private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) {
if (old != updated && updated != null ) {
updated.addOnScrollListener(this);
}
return updated;
}
private void setupDivider() {
Resources res = mHeaderView.getResources();
int verticalGap = res.getDimensionPixelSize(R.dimen.all_apps_divider_margin_vertical);
int sideGap = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
mDivider.setPadding(sideGap, verticalGap,sideGap, mTopOnlyMode ? verticalGap : 0);
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mDivider.getLayoutParams();
lp.removeRule(RelativeLayout.ALIGN_BOTTOM);
lp.addRule(RelativeLayout.ALIGN_BOTTOM, mTopOnlyMode ? R.id.header_content : R.id.tabs);
mDivider.setLayoutParams(lp);
}
public void setMainActive(boolean active) {
mMainRVActive = active;
mSnappedScrolledY = getCurrentScroll() - mMaxTranslation;
setExpanded(true);
}
public View getHeaderView() {
return mHeaderView;
}
public PredictionRowView getContentView() {
return mPredictionRow;
}
public ViewGroup getTabLayout() {
return mTabLayout;
}
public View getDivider() {
return mDivider;
}
@Override
public void onScrolled(RecyclerView rv, int dx, int dy) {
boolean isMainRV = rv == mMainRV;
if (isMainRV != mMainRVActive) {
return;
}
if (mAnimator.isStarted()) {
mAnimator.cancel();
}
int current = - (isMainRV
? mMainRV.getCurrentScrollY()
: mWorkRV.getCurrentScrollY());
moved(current);
apply();
}
public void reset() {
mMainScrolledY = 0;
mWorkScrolledY = 0;
setExpanded(true);
}
private boolean canSnapAt(int currentScrollY) {
boolean snapOnlyOnTop = SHOW_PREDICTIONS_ONLY_ON_TOP || mTopOnlyMode;
return !snapOnlyOnTop || Math.abs(currentScrollY) <= mPredictionRow.getHeight();
}
private void moved(final int currentScrollY) {
if (mHeaderHidden) {
if (currentScrollY <= mSnappedScrolledY) {
if (canSnapAt(currentScrollY)) {
mSnappedScrolledY = currentScrollY;
}
} else {
mHeaderHidden = false;
}
mTranslationY = currentScrollY;
} else if (!mHeaderHidden) {
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
mHeaderHidden = true;
mSnappedScrolledY = currentScrollY;
}
}
}
private void apply() {
int uncappedTranslationY = mTranslationY;
mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
if (mTranslationY != uncappedTranslationY) {
// we hide it completely if already capped (for opening search anim)
mPredictionRow.setVisibility(View.INVISIBLE);
} else {
mPredictionRow.setVisibility(View.VISIBLE);
mPredictionRow.setTranslationY(uncappedTranslationY);
}
mTabLayout.setTranslationY(mTranslationY);
mDivider.setTranslationY(mTopOnlyMode ? uncappedTranslationY : mTranslationY);
mClip.top = mMaxTranslation + mTranslationY;
// clipping on a draw might cause additional redraw
mMainRV.setClipBounds(mClip);
if (mWorkRV != null) {
mWorkRV.setClipBounds(mClip);
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (SHOW_PREDICTIONS_ONLY_ON_TOP) {
return;
}
if (!mTopOnlyMode && newState == RecyclerView.SCROLL_STATE_IDLE
&& mTranslationY != -mMaxTranslation && mTranslationY != 0) {
float scroll = Math.abs(getCurrentScroll());
boolean expand = scroll > mMaxTranslation
? Math.abs(mTranslationY) < mMaxTranslation / 2 : true;
setExpanded(expand);
}
}
private void setExpanded(boolean expand) {
int translateTo = expand ? 0 : -mMaxTranslation;
mAnimator.setIntValues(mTranslationY, translateTo);
mAnimator.addUpdateListener(this);
mAnimator.setDuration(150);
mAnimator.start();
mHeaderHidden = !expand;
mSnappedScrolledY = expand ? getCurrentScroll() - mMaxTranslation : getCurrentScroll();
}
public boolean isExpanded() {
return !mHeaderHidden;
}
private int getCurrentScroll() {
return mMainRVActive ? mMainScrolledY : mWorkScrolledY;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mTranslationY = (Integer) animation.getAnimatedValue();
apply();
}
}