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