Home | History | Annotate | Download | only in widget
      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 
     17 package androidx.leanback.widget;
     18 
     19 import static androidx.recyclerview.widget.RecyclerView.LayoutManager;
     20 import static androidx.recyclerview.widget.RecyclerView.OnScrollListener;
     21 import static androidx.recyclerview.widget.RecyclerView.ViewHolder;
     22 
     23 import android.graphics.Rect;
     24 import android.util.Property;
     25 import android.view.View;
     26 
     27 import 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  */
     34 public 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