1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.ui.widget;
6 
7 import android.graphics.Rect;
8 import android.view.View;
9 import android.view.ViewTreeObserver;
10 
11 import androidx.core.view.ViewCompat;
12 
13 /**
14  * Provides a {@Rect} for the location of a {@View} in its window, see
15  * {@link View#getLocationOnScreen(int[])}.
16  */
17 public class ViewRectProvider extends RectProvider
18         implements ViewTreeObserver.OnGlobalLayoutListener, View.OnAttachStateChangeListener,
19                    ViewTreeObserver.OnPreDrawListener {
20     private final int[] mCachedWindowCoordinates = new int[2];
21     private final Rect mInsetRect = new Rect();
22     private final View mView;
23 
24     /** If not {@code null}, the {@link ViewTreeObserver} that we are registered to. */
25     private ViewTreeObserver mViewTreeObserver;
26 
27     private boolean mIncludePadding;
28 
29     /**
30      * Creates an instance of a {@link ViewRectProvider}.
31      * @param view The {@link View} used to generate a {@link Rect}.
32      */
ViewRectProvider(View view)33     public ViewRectProvider(View view) {
34         mView = view;
35         mCachedWindowCoordinates[0] = -1;
36         mCachedWindowCoordinates[1] = -1;
37     }
38 
39     /**
40      * Specifies the inset values in pixels that determine how to shrink the {@link View} bounds
41      * when creating the {@link Rect}.
42      */
setInsetPx(int left, int top, int right, int bottom)43     public void setInsetPx(int left, int top, int right, int bottom) {
44         mInsetRect.set(left, top, right, bottom);
45         refreshRectBounds();
46     }
47 
48     /**
49      * Specifies the inset values in pixels that determine how to shrink the {@link View} bounds
50      * when creating the {@link Rect}.
51      */
setInsetPx(Rect insetRect)52     public void setInsetPx(Rect insetRect) {
53         mInsetRect.set(insetRect);
54         refreshRectBounds();
55     }
56 
57     /**
58      * Whether padding should be included in the {@link Rect} for the {@link View}.
59      * @param includePadding Whether padding should be included. Defaults to false.
60      */
setIncludePadding(boolean includePadding)61     public void setIncludePadding(boolean includePadding) {
62         mIncludePadding = includePadding;
63     }
64 
65     @Override
startObserving(Observer observer)66     public void startObserving(Observer observer) {
67         mView.addOnAttachStateChangeListener(this);
68         mViewTreeObserver = mView.getViewTreeObserver();
69         mViewTreeObserver.addOnGlobalLayoutListener(this);
70         mViewTreeObserver.addOnPreDrawListener(this);
71 
72         refreshRectBounds();
73 
74         super.startObserving(observer);
75     }
76 
77     @Override
stopObserving()78     public void stopObserving() {
79         mView.removeOnAttachStateChangeListener(this);
80 
81         if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
82             mViewTreeObserver.removeOnGlobalLayoutListener(this);
83             mViewTreeObserver.removeOnPreDrawListener(this);
84         }
85         mViewTreeObserver = null;
86 
87         super.stopObserving();
88     }
89 
90     // ViewTreeObserver.OnGlobalLayoutListener implementation.
91     @Override
onGlobalLayout()92     public void onGlobalLayout() {
93         if (!mView.isShown()) notifyRectHidden();
94     }
95 
96     // ViewTreeObserver.OnPreDrawListener implementation.
97     @Override
onPreDraw()98     public boolean onPreDraw() {
99         if (!mView.isShown()) {
100             notifyRectHidden();
101         } else {
102             refreshRectBounds();
103         }
104 
105         return true;
106     }
107 
108     // View.OnAttachStateChangedObserver implementation.
109     @Override
onViewAttachedToWindow(View v)110     public void onViewAttachedToWindow(View v) {}
111 
112     @Override
onViewDetachedFromWindow(View v)113     public void onViewDetachedFromWindow(View v) {
114         notifyRectHidden();
115     }
116 
refreshRectBounds()117     private void refreshRectBounds() {
118         int previousPositionX = mCachedWindowCoordinates[0];
119         int previousPositionY = mCachedWindowCoordinates[1];
120         mView.getLocationInWindow(mCachedWindowCoordinates);
121 
122         mCachedWindowCoordinates[0] = Math.max(mCachedWindowCoordinates[0], 0);
123         mCachedWindowCoordinates[1] = Math.max(mCachedWindowCoordinates[1], 0);
124 
125         // Return if the window coordinates haven't changed.
126         if (mCachedWindowCoordinates[0] == previousPositionX
127                 && mCachedWindowCoordinates[1] == previousPositionY) {
128             return;
129         }
130 
131         mRect.left = mCachedWindowCoordinates[0];
132         mRect.top = mCachedWindowCoordinates[1];
133         mRect.right = mRect.left + mView.getWidth();
134         mRect.bottom = mRect.top + mView.getHeight();
135 
136         mRect.left += mInsetRect.left;
137         mRect.top += mInsetRect.top;
138         mRect.right -= mInsetRect.right;
139         mRect.bottom -= mInsetRect.bottom;
140 
141         // Account for the padding.
142         if (!mIncludePadding) {
143             boolean isRtl = mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
144             mRect.left +=
145                     isRtl ? ViewCompat.getPaddingEnd(mView) : ViewCompat.getPaddingStart(mView);
146             mRect.right -=
147                     isRtl ? ViewCompat.getPaddingStart(mView) : ViewCompat.getPaddingEnd(mView);
148             mRect.top += mView.getPaddingTop();
149             mRect.bottom -= mView.getPaddingBottom();
150         }
151 
152         // Make sure we still have a valid Rect after applying the inset.
153         mRect.right = Math.max(mRect.left, mRect.right);
154         mRect.bottom = Math.max(mRect.top, mRect.bottom);
155 
156         mRect.right = Math.min(mRect.right, mView.getRootView().getWidth());
157         mRect.bottom = Math.min(mRect.bottom, mView.getRootView().getHeight());
158 
159         notifyRectChanged();
160     }
161 }
162