1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.android.launcher3.allapps; 17 18import static com.android.launcher3.anim.Interpolators.LINEAR; 19 20import android.animation.ValueAnimator; 21import android.content.Context; 22import android.graphics.Point; 23import android.graphics.Rect; 24import android.support.annotation.NonNull; 25import android.support.annotation.Nullable; 26import android.support.v7.widget.RecyclerView; 27import android.util.AttributeSet; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.ViewGroup; 31import android.widget.LinearLayout; 32 33import com.android.launcher3.R; 34import com.android.launcher3.anim.PropertySetter; 35 36public class FloatingHeaderView extends LinearLayout implements 37 ValueAnimator.AnimatorUpdateListener { 38 39 private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE); 40 private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0); 41 private final Point mTempOffset = new Point(); 42 private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { 43 @Override 44 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 45 } 46 47 @Override 48 public void onScrolled(RecyclerView rv, int dx, int dy) { 49 if (rv != mCurrentRV) { 50 return; 51 } 52 53 if (mAnimator.isStarted()) { 54 mAnimator.cancel(); 55 } 56 57 int current = -mCurrentRV.getCurrentScrollY(); 58 moved(current); 59 apply(); 60 } 61 }; 62 63 protected ViewGroup mTabLayout; 64 private AllAppsRecyclerView mMainRV; 65 private AllAppsRecyclerView mWorkRV; 66 private AllAppsRecyclerView mCurrentRV; 67 private ViewGroup mParent; 68 private boolean mHeaderCollapsed; 69 private int mSnappedScrolledY; 70 private int mTranslationY; 71 72 private boolean mAllowTouchForwarding; 73 private boolean mForwardToRecyclerView; 74 75 protected boolean mTabsHidden; 76 protected int mMaxTranslation; 77 private boolean mMainRVActive = true; 78 79 public FloatingHeaderView(@NonNull Context context) { 80 this(context, null); 81 } 82 83 public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) { 84 super(context, attrs); 85 } 86 87 @Override 88 protected void onFinishInflate() { 89 super.onFinishInflate(); 90 mTabLayout = findViewById(R.id.tabs); 91 } 92 93 public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) { 94 mTabsHidden = tabsHidden; 95 mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE); 96 mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView); 97 mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView); 98 mParent = (ViewGroup) mMainRV.getParent(); 99 setMainActive(mMainRVActive || mWorkRV == null); 100 reset(false); 101 } 102 103 private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) { 104 if (old != updated && updated != null ) { 105 updated.addOnScrollListener(mOnScrollListener); 106 } 107 return updated; 108 } 109 110 public void setMainActive(boolean active) { 111 mCurrentRV = active ? mMainRV : mWorkRV; 112 mMainRVActive = active; 113 } 114 115 public int getMaxTranslation() { 116 if (mMaxTranslation == 0 && mTabsHidden) { 117 return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding); 118 } else if (mMaxTranslation > 0 && mTabsHidden) { 119 return mMaxTranslation + getPaddingTop(); 120 } else { 121 return mMaxTranslation; 122 } 123 } 124 125 private boolean canSnapAt(int currentScrollY) { 126 return Math.abs(currentScrollY) <= mMaxTranslation; 127 } 128 129 private void moved(final int currentScrollY) { 130 if (mHeaderCollapsed) { 131 if (currentScrollY <= mSnappedScrolledY) { 132 if (canSnapAt(currentScrollY)) { 133 mSnappedScrolledY = currentScrollY; 134 } 135 } else { 136 mHeaderCollapsed = false; 137 } 138 mTranslationY = currentScrollY; 139 } else if (!mHeaderCollapsed) { 140 mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation; 141 142 // update state vars 143 if (mTranslationY >= 0) { // expanded: must not move down further 144 mTranslationY = 0; 145 mSnappedScrolledY = currentScrollY - mMaxTranslation; 146 } else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden 147 mHeaderCollapsed = true; 148 mSnappedScrolledY = -mMaxTranslation; 149 } 150 } 151 } 152 153 protected void applyScroll(int uncappedY, int currentY) { } 154 155 protected void apply() { 156 int uncappedTranslationY = mTranslationY; 157 mTranslationY = Math.max(mTranslationY, -mMaxTranslation); 158 applyScroll(uncappedTranslationY, mTranslationY); 159 mTabLayout.setTranslationY(mTranslationY); 160 mClip.top = mMaxTranslation + mTranslationY; 161 // clipping on a draw might cause additional redraw 162 mMainRV.setClipBounds(mClip); 163 if (mWorkRV != null) { 164 mWorkRV.setClipBounds(mClip); 165 } 166 } 167 168 public void reset(boolean animate) { 169 if (mAnimator.isStarted()) { 170 mAnimator.cancel(); 171 } 172 if (animate) { 173 mAnimator.setIntValues(mTranslationY, 0); 174 mAnimator.addUpdateListener(this); 175 mAnimator.setDuration(150); 176 mAnimator.start(); 177 } else { 178 mTranslationY = 0; 179 apply(); 180 } 181 mHeaderCollapsed = false; 182 mSnappedScrolledY = -mMaxTranslation; 183 mCurrentRV.scrollToTop(); 184 } 185 186 public boolean isExpanded() { 187 return !mHeaderCollapsed; 188 } 189 190 @Override 191 public void onAnimationUpdate(ValueAnimator animation) { 192 mTranslationY = (Integer) animation.getAnimatedValue(); 193 apply(); 194 } 195 196 @Override 197 public boolean onInterceptTouchEvent(MotionEvent ev) { 198 if (!mAllowTouchForwarding) { 199 mForwardToRecyclerView = false; 200 return super.onInterceptTouchEvent(ev); 201 } 202 calcOffset(mTempOffset); 203 ev.offsetLocation(mTempOffset.x, mTempOffset.y); 204 mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev); 205 ev.offsetLocation(-mTempOffset.x, -mTempOffset.y); 206 return mForwardToRecyclerView || super.onInterceptTouchEvent(ev); 207 } 208 209 @Override 210 public boolean onTouchEvent(MotionEvent event) { 211 if (mForwardToRecyclerView) { 212 // take this view's and parent view's (view pager) location into account 213 calcOffset(mTempOffset); 214 event.offsetLocation(mTempOffset.x, mTempOffset.y); 215 try { 216 return mCurrentRV.onTouchEvent(event); 217 } finally { 218 event.offsetLocation(-mTempOffset.x, -mTempOffset.y); 219 } 220 } else { 221 return super.onTouchEvent(event); 222 } 223 } 224 225 private void calcOffset(Point p) { 226 p.x = getLeft() - mCurrentRV.getLeft() - mParent.getLeft(); 227 p.y = getTop() - mCurrentRV.getTop() - mParent.getTop(); 228 } 229 230 public void setContentVisibility(boolean hasHeader, boolean hasContent, PropertySetter setter) { 231 setter.setViewAlpha(this, hasContent ? 1 : 0, LINEAR); 232 allowTouchForwarding(hasContent); 233 } 234 235 protected void allowTouchForwarding(boolean allow) { 236 mAllowTouchForwarding = allow; 237 } 238 239 public boolean hasVisibleContent() { 240 return false; 241 } 242 243 @Override 244 public boolean hasOverlappingRendering() { 245 return false; 246 } 247} 248 249 250