1 /* 2 * Copyright (C) 2015 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 */ 16 package android.support.car.ui; 17 18 import android.content.Context; 19 import android.graphics.Canvas; 20 import android.os.Parcel; 21 import android.os.Parcelable; 22 import android.support.annotation.NonNull; 23 import android.support.v7.widget.RecyclerView; 24 import android.util.AttributeSet; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import java.lang.reflect.Constructor; 30 import java.lang.reflect.InvocationTargetException; 31 32 /** 33 * Custom {@link RecyclerView} that helps {@link CarLayoutManager} properly fling and paginate. 34 * 35 * It also has the ability to fade children as they scroll off screen that can be set 36 * with {@link #setFadeLastItem(boolean)}. 37 */ 38 public class CarRecyclerView extends RecyclerView { 39 private static final String PARCEL_CLASS = "android.os.Parcel"; 40 private static final String SAVED_STATE_CLASS = 41 "android.support.v7.widget.RecyclerView.SavedState"; 42 private boolean mFadeLastItem; 43 private Constructor<?> mSavedStateConstructor; 44 /** 45 * If the user releases the list with a velocity of 0, {@link #fling(int, int)} will not be 46 * called. However, we want to make sure that the list still snaps to the next page when this 47 * happens. 48 */ 49 private boolean mWasFlingCalledForGesture; 50 CarRecyclerView(Context context)51 public CarRecyclerView(Context context) { 52 this(context, null); 53 } 54 CarRecyclerView(Context context, AttributeSet attrs)55 public CarRecyclerView(Context context, AttributeSet attrs) { 56 this(context, attrs, 0); 57 } 58 CarRecyclerView(Context context, AttributeSet attrs, int defStyle)59 public CarRecyclerView(Context context, AttributeSet attrs, int defStyle) { 60 super(context, attrs, defStyle); 61 setFocusableInTouchMode(false); 62 setFocusable(false); 63 } 64 65 @Override onRestoreInstanceState(Parcelable state)66 protected void onRestoreInstanceState(Parcelable state) { 67 if (state.getClass().getClassLoader() != getClass().getClassLoader()) { 68 if (mSavedStateConstructor == null) { 69 mSavedStateConstructor = getSavedStateConstructor(); 70 } 71 // Class loader mismatch, recreate from parcel. 72 Parcel obtain = Parcel.obtain(); 73 state.writeToParcel(obtain, 0); 74 try { 75 Parcelable newState = (Parcelable) mSavedStateConstructor.newInstance(obtain); 76 super.onRestoreInstanceState(newState); 77 } catch (InstantiationException | IllegalAccessException | IllegalArgumentException 78 | InvocationTargetException e) { 79 // Fail loudy here. 80 throw new RuntimeException(e); 81 } 82 } else { 83 super.onRestoreInstanceState(state); 84 } 85 } 86 87 @Override fling(int velocityX, int velocityY)88 public boolean fling(int velocityX, int velocityY) { 89 mWasFlingCalledForGesture = true; 90 return ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY); 91 } 92 93 @Override onTouchEvent(MotionEvent e)94 public boolean onTouchEvent(MotionEvent e) { 95 // We want the parent to handle all touch events. There's a lot going on there, 96 // and there is no reason to overwrite that functionality. If we do, bad things will happen. 97 final boolean ret = super.onTouchEvent(e); 98 99 int action = e.getActionMasked(); 100 if (action == MotionEvent.ACTION_UP) { 101 if (!mWasFlingCalledForGesture) { 102 ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, 0); 103 } 104 mWasFlingCalledForGesture = false; 105 } 106 107 return ret; 108 } 109 110 @Override drawChild(@onNull Canvas canvas, @NonNull View child, long drawingTime)111 public boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) { 112 if (mFadeLastItem) { 113 float onScreen = 1f; 114 if ((child.getTop() < getBottom() && child.getBottom() > getBottom())) { 115 onScreen = ((float) (getBottom() - child.getTop())) / (float) child.getHeight(); 116 } else if ((child.getTop() < getTop() && child.getBottom() > getTop())) { 117 onScreen = ((float) (child.getBottom() - getTop())) / (float) child.getHeight(); 118 } 119 float alpha = 1 - (1 - onScreen) * (1 - onScreen); 120 fadeChild(child, alpha); 121 } 122 123 return super.drawChild(canvas, child, drawingTime); 124 } 125 setFadeLastItem(boolean fadeLastItem)126 public void setFadeLastItem(boolean fadeLastItem) { 127 mFadeLastItem = fadeLastItem; 128 } 129 pageUp()130 public void pageUp() { 131 CarLayoutManager lm = (CarLayoutManager) getLayoutManager(); 132 int pageUpPosition = lm.getPageUpPosition(); 133 if (pageUpPosition == -1) { 134 return; 135 } 136 137 smoothScrollToPosition(pageUpPosition); 138 } 139 pageDown()140 public void pageDown() { 141 CarLayoutManager lm = (CarLayoutManager) getLayoutManager(); 142 int pageDownPosition = lm.getPageDownPosition(); 143 if (pageDownPosition == -1) { 144 return; 145 } 146 147 smoothScrollToPosition(pageDownPosition); 148 } 149 150 /** 151 * Sets {@link #mSavedStateConstructor} to private SavedState constructor. 152 */ getSavedStateConstructor()153 private Constructor<?> getSavedStateConstructor() { 154 Class<?> savedStateClass = null; 155 // Find package private subclass RecyclerView$SavedState. 156 for (Class<?> c : RecyclerView.class.getDeclaredClasses()) { 157 if (c.getCanonicalName().equals(SAVED_STATE_CLASS)) { 158 savedStateClass = c; 159 break; 160 } 161 } 162 if (savedStateClass == null) { 163 throw new RuntimeException("RecyclerView$SavedState not found!"); 164 } 165 // Find constructor that takes a {@link Parcel}. 166 for (Constructor<?> c : savedStateClass.getDeclaredConstructors()) { 167 Class<?>[] parameterTypes = c.getParameterTypes(); 168 if (parameterTypes.length == 1 169 && parameterTypes[0].getCanonicalName().equals(PARCEL_CLASS)) { 170 mSavedStateConstructor = c; 171 mSavedStateConstructor.setAccessible(true); 172 break; 173 } 174 } 175 if (mSavedStateConstructor == null) { 176 throw new RuntimeException("RecyclerView$SavedState constructor not found!"); 177 } 178 return mSavedStateConstructor; 179 } 180 181 /** 182 * Fades child by alpha. If child is a {@link android.view.ViewGroup} then it will recursively fade its 183 * children instead. 184 */ fadeChild(@onNull View child, float alpha)185 private void fadeChild(@NonNull View child, float alpha) { 186 if (child instanceof ViewGroup) { 187 ViewGroup vg = (ViewGroup) child; 188 for (int i = 0; i < vg.getChildCount(); i++) { 189 fadeChild(vg.getChildAt(i), alpha); 190 } 191 } else { 192 child.setAlpha(alpha); 193 } 194 } 195 } 196