Last active
March 15, 2018 08:13
-
-
Save vivchar/d8ad1bc2fc4a93a623402513f6028181 to your computer and use it in GitHub Desktop.
Tooltips for Android
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Tooltip { | |
public static final String TAG = Tooltip.class.getSimpleName(); | |
@NonNull | |
private final Context mContext; | |
@NonNull | |
private final CharSequence mText; | |
private final int mOffset; | |
private final boolean mCancelable; | |
private final boolean mOutsideCancelable; | |
private final boolean mAnchorCancelable; | |
private final boolean mClipping; | |
@NonNull | |
private final View mContentView; | |
@NonNull | |
private final View mAnchorView; | |
@NonNull | |
private final PopupWindow mPopupWindow; | |
@NonNull | |
private final PopupWindow mHelperPopupWindow; | |
@NonNull | |
private final View mHelperContentView; | |
@NonNull | |
private PointF mCurrentLocation = new PointF(); | |
@NonNull | |
private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = this::updateLocation; | |
@NonNull | |
private final View.OnAttachStateChangeListener mOnViewDetachListener = (OnViewDetachListener) this::dismiss; | |
@NonNull | |
private final OnTouchUpListener mOnTouchUpListener = () -> dismiss(); | |
public Tooltip(@NonNull final Context context, | |
@NonNull final View anchorView, | |
@NonNull final CharSequence text, | |
final int offset, | |
final boolean cancelable, | |
final boolean outsideCancelable, | |
final boolean clipping, | |
final boolean anchorCancelable) { | |
mContext = context; | |
mText = text; | |
mOffset = offset; | |
mCancelable = cancelable; | |
mOutsideCancelable = outsideCancelable; | |
mAnchorCancelable = anchorCancelable; | |
mClipping = clipping; | |
mContentView = createContentView(mContext, mText, mCancelable, mOffset); | |
mAnchorView = prepareAnchorView(anchorView); | |
mPopupWindow = createPopupWindow(mContext, mContentView, mCancelable, mOutsideCancelable); | |
mHelperContentView = createHelperView(mContext); | |
mHelperPopupWindow = createHelperPopupWindow(mContext, mHelperContentView); | |
} | |
@NonNull | |
private View createHelperView(final @NonNull Context context) { | |
final View view = new View(context); | |
view.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); | |
return view; | |
} | |
@NonNull | |
private PopupWindow createHelperPopupWindow(@NonNull final Context context, @NonNull final View contentView) { | |
final PopupWindow popupWindow = new PopupWindow(context); | |
popupWindow.setContentView(contentView); | |
popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); | |
popupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); | |
popupWindow.setWidth(0); | |
popupWindow.setHeight(WindowManager.LayoutParams.MATCH_PARENT); | |
return popupWindow; | |
} | |
@NonNull | |
private View prepareAnchorView(@NonNull final View anchorView) { | |
anchorView.addOnAttachStateChangeListener(mOnViewDetachListener); | |
if (mAnchorCancelable) { | |
anchorView.setOnTouchListener(mOnTouchUpListener); | |
} | |
return anchorView; | |
} | |
@NonNull | |
private PopupWindow createPopupWindow(@NonNull final Context context, | |
@NonNull final View contentView, | |
final boolean cancelable, | |
final boolean outsideCancelable) { | |
final PopupWindow popupWindow = new PopupWindow(context); | |
popupWindow.setClippingEnabled(false); | |
popupWindow.setFocusable(false); | |
popupWindow.setOutsideTouchable(outsideCancelable); | |
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); | |
popupWindow.setContentView(contentView); | |
popupWindow.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); | |
popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); | |
popupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); | |
return popupWindow; | |
} | |
@NonNull | |
private View createContentView(@NonNull final Context context, | |
@NonNull final CharSequence text, | |
final boolean cancelable, | |
final int offset) { | |
final TextView textView = new TextView(context); | |
textView.setBackgroundResource(R.drawable.pop_up_b_l); /* set any background to get a correct size */ | |
textView.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); /* to listen when content will measure */ | |
textView.setText(text); | |
textView.setTextColor(Color.WHITE); | |
textView.setTextSize(13); | |
if (cancelable) { | |
textView.setOnClickListener(v -> dismiss()); | |
} | |
return textView; | |
} | |
private void updateLocation() { | |
debug("layout changed, calculating new position."); | |
final PointF location = calculateLocation(); | |
if (!location.equals(mCurrentLocation)) { | |
debug("updateLocation"); | |
mPopupWindow.setClippingEnabled(mClipping); | |
mPopupWindow.update((int) location.x, (int) location.y, mPopupWindow.getWidth(), mPopupWindow.getHeight()); | |
} | |
mCurrentLocation = location; | |
} | |
@NonNull | |
private PointF calculateLocation() { | |
final PointF location = new PointF(); | |
final RectF anchorRect = calculateRectOnScreen(mAnchorView); | |
final RectF contentRect = calculateRectOnScreen(mContentView); | |
final RectF screenRect = getScreenRect(); | |
final boolean leftBias = anchorRect.centerX() < screenRect.centerX(); | |
final boolean rightBias = !leftBias; | |
final boolean topBias = anchorRect.centerY() < screenRect.centerY(); | |
final boolean bottomBias = !topBias; | |
if (leftBias && topBias) { | |
mContentView.setBackgroundResource(R.drawable.pop_up_u_l); | |
location.x = (int) anchorRect.left; | |
location.y = (int) anchorRect.top + anchorRect.height() + mOffset; | |
} else if (leftBias && bottomBias) { | |
mContentView.setBackgroundResource(R.drawable.pop_up_b_l); | |
location.x = (int) anchorRect.left; | |
location.y = (int) anchorRect.top - contentRect.height() - mOffset; | |
} else if (rightBias && topBias) { | |
mContentView.setBackgroundResource(R.drawable.pop_up_u_r); | |
location.x = (int) (anchorRect.left - (contentRect.width() - anchorRect.width())); | |
location.y = (int) anchorRect.bottom + mOffset; | |
} else if (rightBias && bottomBias) { | |
mContentView.setBackgroundResource(R.drawable.pop_up_b_r); | |
location.x = (int) (anchorRect.left - (contentRect.width() - anchorRect.width())); | |
location.y = (int) anchorRect.top - contentRect.height() - mOffset; | |
} | |
debug("leftBias && topBias: " + leftBias + " && " + topBias); | |
debug("anchorRect: " + anchorRect + ", width: " + anchorRect.width() + ", height: " + anchorRect.height()); | |
debug("contentRect: " + contentRect + ", width: " + contentRect.width() + ", height: " + contentRect.height()); | |
debug("screenRect: " + screenRect + ", width: " + screenRect.width() + ", height: " + screenRect.height()); | |
debug("location: " + location); | |
return location; | |
} | |
public boolean isShowing() { | |
return mPopupWindow.isShowing(); | |
} | |
public void show() { | |
if (!isShowing()) { | |
debug("show"); | |
mHelperPopupWindow.showAtLocation(mAnchorView, Gravity.NO_GRAVITY, 0, 0); | |
if (mClipping) { | |
mAnchorView.post(() -> mPopupWindow.showAsDropDown(mAnchorView)); | |
} else { | |
mAnchorView.post(() -> mPopupWindow.showAtLocation(mAnchorView, Gravity.NO_GRAVITY, 0, 0)); | |
} | |
} | |
} | |
public void dismiss() { | |
debug("dismiss"); | |
mAnchorView.setOnTouchListener(null); | |
mAnchorView.removeOnAttachStateChangeListener(mOnViewDetachListener); | |
removeOnGlobalLayoutListener(mContentView, mOnGlobalLayoutListener); | |
removeOnGlobalLayoutListener(mHelperContentView , mOnGlobalLayoutListener); | |
mPopupWindow.dismiss(); | |
mHelperPopupWindow.dismiss(); | |
} | |
private interface OnTouchUpListener extends View.OnTouchListener { | |
@Override | |
default boolean onTouch(final View v, final MotionEvent event) { | |
if (event.getAction() == MotionEvent.ACTION_UP) { | |
onTouchUp(); | |
} | |
return false; | |
} | |
void onTouchUp(); | |
} | |
private interface OnViewDetachListener extends View.OnAttachStateChangeListener { | |
@Override | |
default void onViewAttachedToWindow(final View v) {} | |
@Override | |
default void onViewDetachedFromWindow(final View v) { | |
onViewDetached(); | |
} | |
void onViewDetached(); | |
} | |
private void debug(@NonNull final String message) { | |
PalLog.d(TAG, message); | |
} | |
@NonNull | |
public RectF getScreenRect() { | |
final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); | |
return new RectF(0, 0, metrics.widthPixels, metrics.heightPixels); | |
} | |
public static class Builder { | |
@NonNull | |
private final Context mContext; | |
@NonNull | |
private final View mAnchor; | |
private CharSequence mText = ""; | |
private boolean mCancelable = true; | |
private boolean mOutsideCancelable = false; | |
private int mOffset = dpToPx(-6); | |
private boolean mClipping = false; | |
private boolean mAnchorCancelable = true; | |
public Builder(@NonNull final View anchorView) { | |
mAnchor = anchorView; | |
mContext = anchorView.getContext(); | |
} | |
@NonNull | |
public Builder setOutsideCancelable(final boolean outsideCancelable) { | |
mOutsideCancelable = outsideCancelable; | |
return this; | |
} | |
@NonNull | |
public Builder setCancelable(final boolean cancelable) { | |
mCancelable = cancelable; | |
return this; | |
} | |
@NonNull | |
public Builder setAnchorCancelable(final boolean anchorCancelable) { | |
mAnchorCancelable = anchorCancelable; | |
return this; | |
} | |
@NonNull | |
public Builder setClipping(final boolean clipping) { | |
mClipping = clipping; | |
return this; | |
} | |
@NonNull | |
public Builder setOffset(final int offset) { | |
mOffset = offset; | |
return this; | |
} | |
@NonNull | |
public Builder setText(@NonNull final CharSequence text) { | |
mText = text; | |
return this; | |
} | |
@NonNull | |
public Builder setText(@StringRes final int text) { | |
setText(mContext.getString(text)); | |
return this; | |
} | |
@NonNull | |
public Tooltip build() { | |
return new Tooltip(mContext, mAnchor, mText, mOffset, mCancelable, mOutsideCancelable, mClipping, mAnchorCancelable); | |
} | |
} | |
protected static class Utils { | |
@SuppressWarnings("deprecation") | |
@SuppressLint("ObsoleteSdkInt") | |
public static void removeOnGlobalLayoutListener(@NonNull final View view, | |
@NonNull final ViewTreeObserver.OnGlobalLayoutListener listener) { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | |
view.getViewTreeObserver().removeOnGlobalLayoutListener(listener); | |
} else { | |
view.getViewTreeObserver().removeGlobalOnLayoutListener(listener); | |
} | |
} | |
public static int dpToPx(final int dp) { | |
return (int) (dp * Resources.getSystem().getDisplayMetrics().density); | |
} | |
@NonNull | |
public static RectF calculateRectOnScreen(@NonNull final View view) { | |
final int[] location = new int[2]; | |
view.getLocationOnScreen(location); | |
return new RectF(location[0], location[1], location[0] + view.getMeasuredWidth(), location[1] + view.getMeasuredHeight()); | |
} | |
@NonNull | |
public static RectF calculateRectInWindow(@NonNull final View view) { | |
final int[] location = new int[2]; | |
view.getLocationInWindow(location); | |
return new RectF(location[0], location[1], location[0] + view.getMeasuredWidth(), location[1] + view.getMeasuredHeight()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment