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 com.google.android.setupdesign.view;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.graphics.Canvas;
22 import android.graphics.RectF;
23 import android.os.Build;
24 import android.util.AttributeSet;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.WindowInsets;
28 
29 /**
30  * This class provides sticky header functionality in a recycler view, to use with
31  * SetupWizardIllustration. To use this, add a header tagged with "sticky". The header will continue
32  * to be drawn when the sticky element hits the top of the view.
33  *
34  * <p>There are a few things to note:
35  *
36  * <ol>
37  *   <li>The view does not work well with padding. b/16190933
38  *   <li>If fitsSystemWindows is true, then this will offset the sticking position by the height of
39  *       the system decorations at the top of the screen.
40  * </ol>
41  */
42 public class StickyHeaderRecyclerView extends HeaderRecyclerView {
43 
44   private View sticky;
45   private int statusBarInset = 0;
46   private final RectF stickyRect = new RectF();
47 
StickyHeaderRecyclerView(Context context)48   public StickyHeaderRecyclerView(Context context) {
49     super(context);
50   }
51 
StickyHeaderRecyclerView(Context context, AttributeSet attrs)52   public StickyHeaderRecyclerView(Context context, AttributeSet attrs) {
53     super(context, attrs);
54   }
55 
StickyHeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)56   public StickyHeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
57     super(context, attrs, defStyleAttr);
58   }
59 
60   @Override
onLayout(boolean changed, int l, int t, int r, int b)61   protected void onLayout(boolean changed, int l, int t, int r, int b) {
62     super.onLayout(changed, l, t, r, b);
63     if (sticky == null) {
64       updateStickyView();
65     }
66     if (sticky != null) {
67       final View headerView = getHeader();
68       if (headerView != null && headerView.getHeight() == 0) {
69         headerView.layout(0, -headerView.getMeasuredHeight(), headerView.getMeasuredWidth(), 0);
70       }
71     }
72   }
73 
74   @Override
onMeasure(int widthSpec, int heightSpec)75   protected void onMeasure(int widthSpec, int heightSpec) {
76     super.onMeasure(widthSpec, heightSpec);
77     if (sticky != null) {
78       measureChild(getHeader(), widthSpec, heightSpec);
79     }
80   }
81 
82   /**
83    * Call this method when the "sticky" view has changed, so this view can update its internal
84    * states as well.
85    */
updateStickyView()86   public void updateStickyView() {
87     final View header = getHeader();
88     if (header != null) {
89       sticky = header.findViewWithTag("sticky");
90     }
91   }
92 
93   @Override
draw(Canvas canvas)94   public void draw(Canvas canvas) {
95     super.draw(canvas);
96     if (sticky != null) {
97       final View headerView = getHeader();
98       final int saveCount = canvas.save();
99       // The view to draw when sticking to the top
100       final View drawTarget = headerView != null ? headerView : sticky;
101       // The offset to draw the view at when sticky
102       final int drawOffset = headerView != null ? sticky.getTop() : 0;
103       // Position of the draw target, relative to the outside of the scrollView
104       final int drawTop = drawTarget.getTop();
105       if (drawTop + drawOffset < statusBarInset || !drawTarget.isShown()) {
106         // RecyclerView does not translate the canvas, so we can simply draw at the top
107         stickyRect.set(
108             0,
109             -drawOffset + statusBarInset,
110             drawTarget.getWidth(),
111             drawTarget.getHeight() - drawOffset + statusBarInset);
112         canvas.translate(0, stickyRect.top);
113         canvas.clipRect(0, 0, drawTarget.getWidth(), drawTarget.getHeight());
114         drawTarget.draw(canvas);
115       } else {
116         stickyRect.setEmpty();
117       }
118       canvas.restoreToCount(saveCount);
119     }
120   }
121 
122   @Override
123   @TargetApi(Build.VERSION_CODES.LOLLIPOP)
onApplyWindowInsets(WindowInsets insets)124   public WindowInsets onApplyWindowInsets(WindowInsets insets) {
125     if (getFitsSystemWindows()) {
126       statusBarInset = insets.getSystemWindowInsetTop();
127       insets.replaceSystemWindowInsets(
128           insets.getSystemWindowInsetLeft(),
129           0, /* top */
130           insets.getSystemWindowInsetRight(),
131           insets.getSystemWindowInsetBottom());
132     }
133     return insets;
134   }
135 
136   @Override
dispatchTouchEvent(MotionEvent ev)137   public boolean dispatchTouchEvent(MotionEvent ev) {
138     if (stickyRect.contains(ev.getX(), ev.getY())) {
139       ev.offsetLocation(-stickyRect.left, -stickyRect.top);
140       return getHeader().dispatchTouchEvent(ev);
141     } else {
142       return super.dispatchTouchEvent(ev);
143     }
144   }
145 }
146