0

I would like to know if there is any simple solution to creating an overlay where an element would get highlighted.

So the final result would look something like this:

enter image description here

I would like to avoid using ShowcaseViewLibrary from variety of reason (it doesn't have the look I need, it's no longer supported etc.).

I thought about using FrameLayout but I am not sure how to achieve the highlighted existing element. Also putting the arrows or bubbles to the elements so they connect precisely.

Nikola Despotoski
  • 49,966
  • 15
  • 119
  • 148
Jakub Holovsky
  • 6,543
  • 10
  • 54
  • 98

2 Answers2

0

A quick and easy way would be to make a copy of the Activity you want to demonstrate with overlays added and just show that. It's what I do and it works fine.

Ivan Wooll
  • 4,145
  • 3
  • 23
  • 34
0
/**
 * Created by Nikola D. on 10/1/2015.
 */
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class ShowCaseLayout extends ScrimInsetsFrameLayout {
    private static final long DEFAULT_DURATION = 1000;
    private static final int DEFAULT_RADIUS = 100;
    private Paint mEmptyPaint;
    private AbstractQueue<Pair<String, View>> mTargetQueue;
    private int mLastCenterX = 600;
    private int mLastCenterY = 100;
    private ValueAnimator.AnimatorUpdateListener mAnimatorListenerX = new ValueAnimator.AnimatorUpdateListener() {
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {

            mLastCenterX = (int) animation.getAnimatedValue();
            setWillNotDraw(false);
            postInvalidate();
        }
    };
    private ValueAnimator.AnimatorUpdateListener mAnimatorListenerY = new ValueAnimator.AnimatorUpdateListener() {
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mLastCenterY = (int) animation.getAnimatedValue();
            setWillNotDraw(false);
            postInvalidate();
        }
    };
    private ValueAnimator mCenterAnimatorX;
    private ValueAnimator mCenterAnimatorY;
    private boolean canRender = false;
    private OnAttachStateChangeListener mAttachListener = new OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {
            canRender = true;
        }

        @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
        @Override
        public void onViewDetachedFromWindow(View v) {
            canRender = false;
            removeOnAttachStateChangeListener(this);
        }
    };
    private long mDuration = DEFAULT_DURATION;
    private int mRadius = (int) DEFAULT_RADIUS;
    private Interpolator mInterpolator = new LinearOutSlowInInterpolator();
    private ValueAnimator mRadiusAnimator;
    private ValueAnimator.AnimatorUpdateListener mRadiusAnimatorListener = new ValueAnimator.AnimatorUpdateListener() {
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mRadius = (int) animation.getAnimatedValue();
        }
    };
    private TextView mDescriptionText;
    private Button mGotItButton;
    private OnClickListener mExternalGotItButtonlistener;
    private OnClickListener mGotItButtonClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            setNextTarget();
            if (mExternalGotItButtonlistener != null) {
                mExternalGotItButtonlistener.onClick(v);
            }
        }
    };
    private Animator.AnimatorListener mAnimatorSetListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            setNextTarget();
            invalidate();
            //mDescriptionText.layout(mTempRect.left, mTempRect.bottom + mTempRect.bottom, mDescriptionText. );
        }
    };
    private Rect mTempRect;
    private Paint mBackgroundPaint;
    private Bitmap bitmap;
    private Canvas temp;
    private int mStatusBarHeight = 0;

    public ShowCaseLayout(Context context) {
        super(context);
        setupLayout();
    }

    public ShowCaseLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setupLayout();
    }

    public ShowCaseLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setupLayout();
    }

    public void setTarget(View target, String hint) {
        mTargetQueue.add(new Pair<>(hint, target));
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void setupLayout() {
        mTargetQueue = new LinkedBlockingQueue<>();
        setWillNotDraw(false);
        mBackgroundPaint = new Paint();
        int c = Color.argb(127, Color.red(Color.RED), Color.blue(Color.RED), Color.green(Color.RED));
        mBackgroundPaint.setColor(c);
        mEmptyPaint = new Paint();
        mEmptyPaint.setColor(Color.TRANSPARENT);
        mEmptyPaint.setStyle(Paint.Style.FILL);
        mEmptyPaint.setAntiAlias(true);
        mEmptyPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        if (!ViewCompat.isLaidOut(this))
            addOnAttachStateChangeListener(mAttachListener);
        else canRender = true;
        mDescriptionText = new TextView(getContext());
        mGotItButton = new Button(getContext());
        mGotItButton.setText("GOT IT");
        mGotItButton.setOnClickListener(mGotItButtonClickListener);
        addView(mGotItButton, generateDefaultLayoutParams());
        //ViewCompat.setAlpha(this, 0.5f);

    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!canRender) return;
        temp.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mBackgroundPaint);
        temp.drawCircle(mLastCenterX, mLastCenterY, mRadius, mEmptyPaint);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    @TargetApi(Build.VERSION_CODES.M)
    private void animateCenterToNextTarget(View target) {
        int[] locations = new int[2];
        target.getLocationInWindow(locations);
        int x = locations[0];
        int y = locations[1];
        mTempRect = new Rect(x, y, x + target.getWidth(), y + target.getHeight());
        int centerX = mTempRect.centerX();
        int centerY = mTempRect.centerY();
        int targetRadius = Math.abs(mTempRect.right - mTempRect.left) / 2;
        targetRadius += targetRadius * 0.05;
        mCenterAnimatorX = ValueAnimator.ofInt(mLastCenterX, centerX).setDuration(mDuration);
        mCenterAnimatorX.addUpdateListener(mAnimatorListenerX);
        mCenterAnimatorY = ValueAnimator.ofInt(mLastCenterY, centerY).setDuration(mDuration);
        mCenterAnimatorY.addUpdateListener(mAnimatorListenerY);
        mRadiusAnimator = ValueAnimator.ofInt(mRadius, targetRadius);
        mRadiusAnimator.addUpdateListener(mRadiusAnimatorListener);
        playTogether(mCenterAnimatorY, mCenterAnimatorX, mRadiusAnimator);

    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        bitmap.eraseColor(Color.TRANSPARENT);
        temp = new Canvas(bitmap);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void playTogether(ValueAnimator... animators) {
        AnimatorSet set = new AnimatorSet();
        set.setInterpolator(mInterpolator);
        set.setDuration(mDuration);
        set.playTogether(animators);
        set.addListener(mAnimatorSetListener);
        set.start();
    }

    public void start(Activity activity) {
        if (getParent() == null) {
            attachLayoutToWindow(activity);
        }
        setNextTarget();
    }

    private void setNextTarget() {
        Pair<String, View> pair = mTargetQueue.poll();
        if (pair != null) {
            if (pair.second != null)
                animateCenterToNextTarget(pair.second);
            mDescriptionText.setText(pair.first);
        }
    }

    private void attachLayoutToWindow(Activity activity) {
        FrameLayout rootLayout = (FrameLayout) activity.findViewById(android.R.id.content);
        rootLayout.addView(this);
    }

    public void hideShowcaseLayout() {

    }


    public void setGotItButtonClickistener(OnClickListener mExternalGotItButtonlistener) {
        this.mExternalGotItButtonlistener = mExternalGotItButtonlistener;
    }

    public TextView getDescriptionTextView() {
        return mDescriptionText;
    }

    public void setDescriptionTextView(TextView textView) {
        mDescriptionText = textView;
    }


}

Please note that this code is incomplete and is under development, you should tweak it according your needs.

This layout will draw a circle around the View over its Rect.

Instead of drawing the circle you could drawRect to the Rect bounds of the target view or drawRoundRect if the View's Rect and background drawable Rect are complementary.

Drawing the line (drawLine()) should be from the target view:

startX = (rect.right - rect.left)/2;
startY = rect.bottom;
endX = startX; 
endY = startY  + arbitraryLineHeight;

if the endY is larger than the layout height you should be drawing it upwards rect.top - arbitraryLineHeight, otherwise you draw it as it is.

arbitraryLineHeight could be descriptionViewRect.top which makes it more dynamic, instead of using a constant value.

Nikola Despotoski
  • 49,966
  • 15
  • 119
  • 148