1/*
2 * Copyright (C) 2017 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.v7.widget;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.app.Activity;
22import android.content.Context;
23import android.content.ContextWrapper;
24import android.content.res.Resources;
25import android.graphics.PixelFormat;
26import android.graphics.Rect;
27import android.support.annotation.RestrictTo;
28import android.support.v7.appcompat.R;
29import android.util.DisplayMetrics;
30import android.util.Log;
31import android.view.Gravity;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.WindowManager;
35import android.widget.TextView;
36
37/**
38 * A popup window displaying a text message aligned to a specified view.
39 *
40 * @hide
41 */
42@RestrictTo(LIBRARY_GROUP)
43class TooltipPopup {
44    private static final String TAG = "TooltipPopup";
45
46    private final Context mContext;
47
48    private final View mContentView;
49    private final TextView mMessageView;
50
51    private final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
52    private final Rect mTmpDisplayFrame = new Rect();
53    private final int[] mTmpAnchorPos = new int[2];
54    private final int[] mTmpAppPos = new int[2];
55
56    TooltipPopup(Context context) {
57        mContext = context;
58
59        mContentView = LayoutInflater.from(mContext).inflate(R.layout.tooltip, null);
60        mMessageView = (TextView) mContentView.findViewById(R.id.message);
61
62        mLayoutParams.setTitle(getClass().getSimpleName());
63        mLayoutParams.packageName = mContext.getPackageName();
64        mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
65        mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
66        mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
67        mLayoutParams.format = PixelFormat.TRANSLUCENT;
68        mLayoutParams.windowAnimations = R.style.Animation_AppCompat_Tooltip;
69        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
70                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
71    }
72
73    void show(View anchorView, int anchorX, int anchorY, boolean fromTouch,
74            CharSequence tooltipText) {
75        if (isShowing()) {
76            hide();
77        }
78
79        mMessageView.setText(tooltipText);
80
81        computePosition(anchorView, anchorX, anchorY, fromTouch, mLayoutParams);
82
83        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
84        wm.addView(mContentView, mLayoutParams);
85    }
86
87    void hide() {
88        if (!isShowing()) {
89            return;
90        }
91
92        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
93        wm.removeView(mContentView);
94    }
95
96    boolean isShowing() {
97        return mContentView.getParent() != null;
98    }
99
100    void updateContent(CharSequence tooltipText) {
101        mMessageView.setText(tooltipText);
102    }
103
104    private void computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch,
105            WindowManager.LayoutParams outParams) {
106        final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset(
107                R.dimen.tooltip_precise_anchor_threshold);
108
109        final int offsetX;
110        if (anchorView.getWidth() >= tooltipPreciseAnchorThreshold) {
111            // Wide view. Align the tooltip horizontally to the precise X position.
112            offsetX = anchorX;
113        } else {
114            // Otherwise anchor the tooltip to the view center.
115            offsetX = anchorView.getWidth() / 2;  // Center on the view horizontally.
116        }
117
118        final int offsetBelow;
119        final int offsetAbove;
120        if (anchorView.getHeight() >= tooltipPreciseAnchorThreshold) {
121            // Tall view. Align the tooltip vertically to the precise Y position.
122            final int offsetExtra = mContext.getResources().getDimensionPixelOffset(
123                    R.dimen.tooltip_precise_anchor_extra_offset);
124            offsetBelow = anchorY + offsetExtra;
125            offsetAbove = anchorY - offsetExtra;
126        } else {
127            // Otherwise anchor the tooltip to the view center.
128            offsetBelow = anchorView.getHeight();  // Place below the view in most cases.
129            offsetAbove = 0;  // Place above the view if the tooltip does not fit below.
130        }
131
132        outParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
133
134        final int tooltipOffset = mContext.getResources().getDimensionPixelOffset(
135                fromTouch ? R.dimen.tooltip_y_offset_touch : R.dimen.tooltip_y_offset_non_touch);
136
137        final View appView = getAppRootView(anchorView);
138        if (appView == null) {
139            Log.e(TAG, "Cannot find app view");
140            return;
141        }
142        appView.getWindowVisibleDisplayFrame(mTmpDisplayFrame);
143        if (mTmpDisplayFrame.left < 0 && mTmpDisplayFrame.top < 0) {
144            // No meaningful display frame, the anchor view is probably in a subpanel
145            // (such as a popup window). Use the screen frame as a reasonable approximation.
146            final Resources res = mContext.getResources();
147            final int statusBarHeight;
148            int resourceId = res.getIdentifier("status_bar_height", "dimen", "android");
149            if (resourceId != 0) {
150                statusBarHeight = res.getDimensionPixelSize(resourceId);
151            } else {
152                statusBarHeight = 0;
153            }
154            final DisplayMetrics metrics = res.getDisplayMetrics();
155            mTmpDisplayFrame.set(0, statusBarHeight, metrics.widthPixels, metrics.heightPixels);
156        }
157        appView.getLocationOnScreen(mTmpAppPos);
158
159        anchorView.getLocationOnScreen(mTmpAnchorPos);
160        mTmpAnchorPos[0] -= mTmpAppPos[0];
161        mTmpAnchorPos[1] -= mTmpAppPos[1];
162        // mTmpAnchorPos is now relative to the main app window.
163
164        outParams.x = mTmpAnchorPos[0] + offsetX - mTmpDisplayFrame.width() / 2;
165
166        final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
167        mContentView.measure(spec, spec);
168        final int tooltipHeight = mContentView.getMeasuredHeight();
169
170        final int yAbove = mTmpAnchorPos[1] + offsetAbove - tooltipOffset - tooltipHeight;
171        final int yBelow = mTmpAnchorPos[1] + offsetBelow + tooltipOffset;
172        if (fromTouch) {
173            if (yAbove >= 0) {
174                outParams.y = yAbove;
175            } else {
176                outParams.y = yBelow;
177            }
178        } else {
179            if (yBelow + tooltipHeight <= mTmpDisplayFrame.height()) {
180                outParams.y = yBelow;
181            } else {
182                outParams.y = yAbove;
183            }
184        }
185    }
186
187    private static View getAppRootView(View anchorView) {
188        Context context = anchorView.getContext();
189        while (context instanceof ContextWrapper) {
190            if (context instanceof Activity) {
191                return ((Activity) context).getWindow().getDecorView();
192            } else {
193                context = ((ContextWrapper) context).getBaseContext();
194            }
195        }
196        return anchorView.getRootView();
197    }
198}
199