Пользовательское наложение инструкций с выделенным представлением (без использования ShowcaseViewLibrary)

Я хотел бы знать, есть ли какое-либо простое решение для создания наложения, где элемент будет выделен.

Таким образом, конечный результат будет выглядеть примерно так:

введите описание изображения здесь

Я хотел бы избежать использования ShowcaseViewLibrary по разным причинам (он не выглядит так, как мне нужно, он больше не поддерживается и т. Д.).

Я думал об использовании FrameLayout, но я не уверен, как достичь выделенного существующего элемента. Также добавьте стрелки или пузырьки к элементам, чтобы они точно соединялись.

2 ответа

Быстрый и простой способ - сделать копию действия, которое вы хотите продемонстрировать, с добавлением наложений, и просто показать это. Это то, что я делаю, и это прекрасно работает.

/**
 * 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;
    }


}

Обратите внимание, что этот код неполон и находится в стадии разработки, вы должны настроить его в соответствии со своими потребностями.

Этот макет нарисует круг вокруг View над его Rect,

Вместо рисования круга вы могли бы drawRect к Rect границы целевого вида или drawRoundRect если Rect и задний план View Drawable Rect дополняют друг друга.

Рисование линии (drawLine()) должно быть с точки зрения цели:

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

если endY больше, чем высота макета, вы должны рисовать его вверх rect.top - arbitraryLineHeightв противном случае вы рисуете как есть.

arbitraryLineHeight может быть descriptionViewRect.top что делает его более динамичным, вместо использования постоянного значения.

Другие вопросы по тегам