/* * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.marshalchen.common.uimodule.discreteseekbar.internal; import android.content.Context; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.support.v4.view.GravityCompat; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; import com.marshalchen.common.uimodule.discreteseekbar.internal.drawable.MarkerDrawable; /** * Class to manage the floating bubble thing, similar (but quite worse tested than {@link android.widget.PopupWindow} * <p/> * <p> * This will attach a View to the Window (full-width, measured-height, positioned just under the thumb) * </p> * * @hide * @see #showIndicator(android.view.View, android.graphics.Rect) * @see #dismiss() * @see #dismissComplete() * @see com.marshalchen.common.uimodule.discreteseekbar.internal.PopupIndicator.Floater */ public class PopupIndicator { private final WindowManager mWindowManager; private boolean mShowing; private Floater mPopupView; //Outside listener for the DiscreteSeekBar to get MarkerDrawable animation events. //The whole chain of events goes this way: //MarkerDrawable->Marker->Floater->mListener->DiscreteSeekBar.... //... phew! private MarkerDrawable.MarkerAnimationListener mListener; private int[] mDrawingLocation = new int[2]; Point screenSize = new Point(); public PopupIndicator(Context context, AttributeSet attrs, int defStyleAttr, String maxValue) { mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mPopupView = new Floater(context, attrs, defStyleAttr, maxValue); DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); screenSize.set(displayMetrics.widthPixels, displayMetrics.heightPixels); } public void updateSizes(String maxValue) { dismissComplete(); if (mPopupView != null) { mPopupView.mMarker.resetSizes(maxValue); } } public void setListener(MarkerDrawable.MarkerAnimationListener listener) { mListener = listener; } /** * We want the Floater to be full-width because the contents will be moved from side to side. * We may/should change this in the future to use just the PARENT View width and/or pass it in the constructor */ private void measureFloater() { int specWidth = View.MeasureSpec.makeMeasureSpec(screenSize.x, View.MeasureSpec.EXACTLY); int specHeight = View.MeasureSpec.makeMeasureSpec(screenSize.y, View.MeasureSpec.AT_MOST); mPopupView.measure(specWidth, specHeight); } public void setValue(CharSequence value) { mPopupView.mMarker.setValue(value); } public boolean isShowing() { return mShowing; } public void showIndicator(View parent, Rect touchBounds) { if (isShowing()) { mPopupView.mMarker.animateOpen(); return; } IBinder windowToken = parent.getWindowToken(); if (windowToken != null) { WindowManager.LayoutParams p = createPopupLayout(windowToken); p.gravity = Gravity.TOP | GravityCompat.START; updateLayoutParamsForPosiion(parent, p, touchBounds.bottom); mShowing = true; translateViewIntoPosition(touchBounds.centerX()); invokePopup(p); } } public void move(int x) { if (!isShowing()) { return; } translateViewIntoPosition(x); } /** * This will start the closing animation of the Marker and call onClosingComplete when finished */ public void dismiss() { mPopupView.mMarker.animateClose(); } /** * FORCE the popup window to be removed. * You typically calls this when the parent view is being removed from the window to avoid a Window Leak */ public void dismissComplete() { if (isShowing()) { mShowing = false; try { mWindowManager.removeViewImmediate(mPopupView); } finally { } } } private void updateLayoutParamsForPosiion(View anchor, WindowManager.LayoutParams p, int yOffset) { measureFloater(); int measuredHeight = mPopupView.getMeasuredHeight(); int paddingBottom = mPopupView.mMarker.getPaddingBottom(); anchor.getLocationInWindow(mDrawingLocation); p.x = 0; p.y = mDrawingLocation[1] - measuredHeight + yOffset + paddingBottom; p.width = screenSize.x; p.height = measuredHeight; } private void translateViewIntoPosition(final int x) { mPopupView.setFloatOffset(x + mDrawingLocation[0]); } private void invokePopup(WindowManager.LayoutParams p) { mWindowManager.addView(mPopupView, p); mPopupView.mMarker.animateOpen(); } private WindowManager.LayoutParams createPopupLayout(IBinder token) { WindowManager.LayoutParams p = new WindowManager.LayoutParams(); p.gravity = Gravity.START | Gravity.TOP; p.width = ViewGroup.LayoutParams.MATCH_PARENT; p.height = ViewGroup.LayoutParams.MATCH_PARENT; p.format = PixelFormat.TRANSLUCENT; p.flags = computeFlags(p.flags); p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; p.token = token; p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; p.setTitle("DiscreteSeekBar Indicator:" + Integer.toHexString(hashCode())); return p; } /** * I'm NOT completely sure how all this bitwise things work... * * @param curFlags * @return */ private int computeFlags(int curFlags) { curFlags &= ~( WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; return curFlags; } /** * Small FrameLayout class to hold and move the bubble around when requested * I wanted to use the {@link com.marshalchen.common.uimodule.discreteseekbar.internal.Marker} directly * but doing so would make some things harder to implement * (like moving the marker around, having the Marker's outline to work, etc) */ private class Floater extends FrameLayout implements MarkerDrawable.MarkerAnimationListener { private Marker mMarker; private int mOffset; public Floater(Context context, AttributeSet attrs, int defStyleAttr, String maxValue) { super(context); mMarker = new Marker(context, attrs, defStyleAttr, maxValue); addView(mMarker, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSie = mMarker.getMeasuredHeight(); setMeasuredDimension(widthSize, heightSie); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int centerDiffX = mMarker.getMeasuredWidth() / 2; int offset = (mOffset - centerDiffX); mMarker.layout(offset, 0, offset + mMarker.getMeasuredWidth(), mMarker.getMeasuredHeight()); } public void setFloatOffset(int x) { mOffset = x; int centerDiffX = mMarker.getMeasuredWidth() / 2; int offset = (x - centerDiffX); mMarker.offsetLeftAndRight(offset - mMarker.getLeft()); //On API<11, offsetting a view seems to NOT invalidate the proper area. //We should calc the proper invalidate Rect but this will be for now... if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { invalidate(); } } @Override public void onClosingComplete() { if (mListener != null) { mListener.onClosingComplete(); } dismissComplete(); } @Override public void onOpeningComplete() { if (mListener != null) { mListener.onOpeningComplete(); } } } }