1/* 2 * Copyright (C) 2016 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 17package android.support.v17.leanback.widget; 18 19import static android.support.v7.widget.RecyclerView.LayoutManager; 20import static android.support.v7.widget.RecyclerView.OnScrollListener; 21import static android.support.v7.widget.RecyclerView.ViewHolder; 22 23import android.graphics.Rect; 24import android.support.v7.widget.RecyclerView; 25import android.util.Property; 26import android.view.View; 27 28/** 29 * Implementation of {@link Parallax} class for {@link RecyclerView}. This class 30 * allows users to track position of specific views inside {@link RecyclerView} relative to 31 * itself. @see {@link ChildPositionProperty} for details. 32 */ 33public class RecyclerViewParallax extends Parallax<RecyclerViewParallax.ChildPositionProperty> { 34 RecyclerView mRecylerView; 35 boolean mIsVertical; 36 37 OnScrollListener mOnScrollListener = new OnScrollListener() { 38 @Override 39 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 40 updateValues(); 41 } 42 }; 43 44 View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() { 45 @Override 46 public void onLayoutChange(View view, int l, int t, int r, int b, 47 int oldL, int oldT, int oldR, int oldB) { 48 updateValues(); 49 } 50 }; 51 52 /** 53 * Subclass of {@link Parallax.IntProperty}. Using this Property, users can track a 54 * RecylerView child's position inside recyclerview. i.e. 55 * 56 * tracking_pos = view.top + fraction * view.height() + offset 57 * 58 * This way we can track top using fraction 0 and bottom using fraction 1. 59 */ 60 public static final class ChildPositionProperty extends Parallax.IntProperty { 61 int mAdapterPosition; 62 int mViewId; 63 int mOffset; 64 float mFraction; 65 66 ChildPositionProperty(String name, int index) { 67 super(name, index); 68 } 69 70 /** 71 * Sets adapter position of the recyclerview child to track. 72 * 73 * @param adapterPosition Zero based position in adapter. 74 * @return This ChildPositionProperty object. 75 */ 76 public ChildPositionProperty adapterPosition(int adapterPosition) { 77 mAdapterPosition = adapterPosition; 78 return this; 79 }; 80 81 /** 82 * Sets view Id of a descendant of recyclerview child to track. 83 * 84 * @param viewId Id of a descendant of recyclerview child. 85 * @return This ChildPositionProperty object. 86 */ 87 public ChildPositionProperty viewId(int viewId) { 88 mViewId = viewId; 89 return this; 90 } 91 92 /** 93 * Sets offset in pixels added to the view's start position. 94 * 95 * @param offset Offset in pixels added to the view's start position. 96 * @return This ChildPositionProperty object. 97 */ 98 public ChildPositionProperty offset(int offset) { 99 mOffset = offset; 100 return this; 101 } 102 103 /** 104 * Sets fraction of size to be added to view's start position. e.g. to track the 105 * center position of the view, use fraction 0.5; to track the end position of the view 106 * use fraction 1. 107 * 108 * @param fraction Fraction of size of the view. 109 * @return This ChildPositionProperty object. 110 */ 111 public ChildPositionProperty fraction(float fraction) { 112 mFraction = fraction; 113 return this; 114 } 115 116 /** 117 * Returns adapter position of the recyclerview child to track. 118 */ 119 public int getAdapterPosition() { 120 return mAdapterPosition; 121 } 122 123 /** 124 * Returns view Id of a descendant of recyclerview child to track. 125 */ 126 public int getViewId() { 127 return mViewId; 128 } 129 130 /** 131 * Returns offset in pixels added to the view's start position. 132 */ 133 public int getOffset() { 134 return mOffset; 135 } 136 137 /** 138 * Returns fraction of size to be added to view's start position. e.g. to track the 139 * center position of the view, use fraction 0.5; to track the end position of the view 140 * use fraction 1. 141 */ 142 public float getFraction() { 143 return mFraction; 144 } 145 146 void updateValue(RecyclerViewParallax source) { 147 RecyclerView recyclerView = source.mRecylerView; 148 ViewHolder viewHolder = recyclerView == null ? null 149 : recyclerView.findViewHolderForAdapterPosition(mAdapterPosition); 150 if (viewHolder == null) { 151 if (recyclerView == null || recyclerView.getLayoutManager().getChildCount() == 0) { 152 source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_AFTER); 153 return; 154 } 155 View firstChild = recyclerView.getLayoutManager().getChildAt(0); 156 ViewHolder vh = recyclerView.findContainingViewHolder(firstChild); 157 int firstPosition = vh.getAdapterPosition(); 158 if (firstPosition < mAdapterPosition) { 159 source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_AFTER); 160 } else { 161 source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_BEFORE); 162 } 163 } else { 164 View trackingView = viewHolder.itemView.findViewById(mViewId); 165 if (trackingView == null) { 166 return; 167 } 168 169 Rect rect = new Rect( 170 0, 0, trackingView.getWidth(), trackingView.getHeight()); 171 recyclerView.offsetDescendantRectToMyCoords(trackingView, rect); 172 // Slide transition may change the trackingView's translationX/translationY, 173 // add up translation values in parent. 174 float tx = 0, ty = 0; 175 while (trackingView != recyclerView && trackingView != null) { 176 // In RecyclerView dispatchLayout() it may call onScrolled(0) with a move 177 // ItemAnimation just created. We don't have any way to track the ItemAnimation 178 // update listener, and in ideal use case, the tracking view should not be 179 // animated in RecyclerView. Do not apply translation value for this case. 180 if (!(trackingView.getParent() == recyclerView && recyclerView.isAnimating())) { 181 tx += trackingView.getTranslationX(); 182 ty += trackingView.getTranslationY(); 183 } 184 trackingView = (View) trackingView.getParent(); 185 } 186 rect.offset((int) tx, (int) ty); 187 if (source.mIsVertical) { 188 source.setIntPropertyValue(getIndex(), rect.top + mOffset 189 + (int) (mFraction * rect.height())); 190 } else { 191 source.setIntPropertyValue(getIndex(), rect.left + mOffset 192 + (int) (mFraction * rect.width())); 193 } 194 } 195 } 196 } 197 198 199 @Override 200 public ChildPositionProperty createProperty(String name, int index) { 201 return new ChildPositionProperty(name, index); 202 } 203 204 @Override 205 public float getMaxValue() { 206 if (mRecylerView == null) { 207 return 0; 208 } 209 return mIsVertical ? mRecylerView.getHeight() : mRecylerView.getWidth(); 210 } 211 212 /** 213 * Set RecyclerView that this Parallax will register onScrollListener. 214 * @param recyclerView RecyclerView to register onScrollListener. 215 */ 216 public void setRecyclerView(RecyclerView recyclerView) { 217 if (mRecylerView == recyclerView) { 218 return; 219 } 220 if (mRecylerView != null) { 221 mRecylerView.removeOnScrollListener(mOnScrollListener); 222 mRecylerView.removeOnLayoutChangeListener(mOnLayoutChangeListener); 223 } 224 mRecylerView = recyclerView; 225 if (mRecylerView != null) { 226 LayoutManager.Properties properties = mRecylerView.getLayoutManager() 227 .getProperties(mRecylerView.getContext(), null, 0, 0); 228 mIsVertical = properties.orientation == RecyclerView.VERTICAL; 229 mRecylerView.addOnScrollListener(mOnScrollListener); 230 mRecylerView.addOnLayoutChangeListener(mOnLayoutChangeListener); 231 } 232 } 233 234 /** 235 * Manually update values. This is used for changes not controlled by RecyclerView. E.g. 236 * called by a Slide transition that changes translation of the view. 237 */ 238 @Override 239 public void updateValues() { 240 for (Property prop: getProperties()) { 241 ((ChildPositionProperty) prop).updateValue(RecyclerViewParallax.this); 242 } 243 super.updateValues(); 244 } 245 246 /** 247 * @return Currently RecylerView that the source has registered onScrollListener. 248 */ 249 public RecyclerView getRecyclerView() { 250 return mRecylerView; 251 } 252} 253