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