Android 强大实用的功能引导组件

作者: SwitchLife | 来源:发表于2018-08-06 17:03 被阅读112次

    开篇

      利用闲暇时间撸了一个功能引导组件,此组件将在0.4.4版本里添加。小弟不才,一心只想打造一个能搞定一个应用的基础库。希望童鞋们多多支持!

    • 引导波纹动画(可个性化配置,如颜色、动画速度、大小等)
    • 引导画面的上按钮与需被引导的按钮同等样式(如大小、颜色、形状)
    • 支持动态添加各种需要显示的view.

    效果截屏

    立即体验

    扫描以下二维码下载体验App(从0.2.3版本开始,体验App内嵌版本更新检测功能):


    JSCKit库传送门:https://github.com/JustinRoom/JSCKit

    简析源码

    下面依次分享相关组件

    一、GuideRippleView

    一个实现循环水波纹动画的自定义view

    其主要逻辑code很简单(绘制、动画控制都在onDraw()方法中)、没有什么难懂的东西,自行看代码理解:

    • protected void onDraw(Canvas canvas)
        @Override
        protected void onDraw(Canvas canvas) {
            //只有开启动画时,才进行绘制。
            if (!isRunning)
                return;
            //这里是限制动画绘制区域
            if (clipWidth > 0 && clipHeight > 0){
                int clipLeft = (getWidth() - clipWidth) / 2;
                int clipTop = (getHeight() - clipHeight) / 2;
                canvas.clipRect(clipLeft, clipTop, clipLeft + clipWidth, clipTop + clipHeight);
            }
    
            //绘制圆圈
            float maxRadius = getWidth() / 2.0f;
            int alpha = (int) (startAlpha * (1 - radius / maxRadius) + .5f);
            for (int i = 0; i < circleCount; i++) {
                paint.setColor(colors[I]);
                paint.setAlpha(alpha);
                float tempRadius = radius - circleSpace * I;
                if (tempRadius <= 0)
                    break;
                canvas.drawCircle(maxRadius, maxRadius, tempRadius, paint);
            }
            //speed这是控制动画速度的参数
            radius += speed;
            if (radius > maxRadius)
                radius = 0;
            invalidate();
        }
    
    二、GuideLayout

    用来控制被引导按钮的显示位置,以及添加其他的子view。

    分析关键code:

        public void updateTargetLocation(@NonNull View target, int yOffset, int minRippleSize, int maxRippleSize, OnRippleViewUpdateLocationCallback callback) {
            Bitmap bitmap = Bitmap.createBitmap(target.getDrawingCache());
            int[] location = new int[2];
            target.getLocationOnScreen(location);
            targetRect.set(location[0], location[1], location[0] + target.getWidth(), location[1] + target.getHeight());
            targetRect.offset(0, -yOffset);
            ivTarget.setImageBitmap(bitmap);
            MarginLayoutParams params = (MarginLayoutParams) ivTarget.getLayoutParams();
            params.leftMargin = targetRect.left;
            params.topMargin = targetRect.top;
            ivTarget.setLayoutParams(params);
            updateRippleLocation(targetRect, minRippleSize, maxRippleSize, callback);
        }
    
        private void updateRippleLocation(@NonNull Rect targetRect, int minRippleSize, int maxRippleSize, OnRippleViewUpdateLocationCallback callback) {
            int size = Math.max(targetRect.width(), targetRect.height());
            int min = Math.min(minRippleSize, maxRippleSize);
            int max = minRippleSize + maxRippleSize - min;
            if (min > 0) {
                size = Math.max(size, minRippleSize);
            }
            if (max > 0) {
                size = Math.min(size, maxRippleSize);
            }
            LayoutParams params = new LayoutParams(size, 0);
            params.leftMargin = (targetRect.left + targetRect.right - size) / 2;
            params.topMargin = (targetRect.top + targetRect.bottom - size) / 2;
            if (guideRippleViewView == null) {
                guideRippleViewView = new GuideRippleView(getContext());
                addView(guideRippleViewView, params);
            } else {
                guideRippleViewView.setLayoutParams(params);
            }
            if (callback != null)
                callback.onRippleViewUpdateLocation(guideRippleViewView);
        }
    

    target——被引导的按钮。
    详细逻辑步骤:

    • 1、获取target在屏幕中的坐标位置
    • 2、获取targetdrawingCache。在getDrawingCache()之前我们需要调用target.setDrawingCacheEnabled(true),使用完drawingCache完之后我们需要关掉target.setDrawingCacheEnabled(false),避免性能损耗。
    • 3、新建一个ImageView并加载从targetdrawingCache中生成的Bitmap
    • 4、添加水波纹动画view。
    三、4种展示功能引导方式

    原理:
    a、ViewGroup root = activity.findViewById(android.R.id.content)
    b、root.addView(guideLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))

        /**
         * @param context       context
         * @param yOffset       offset from top
         * @param minRippleSize minimum ripple size
         * @param maxRippleSize max ripple size
         */
        public GuidePopupView(Context context, int yOffset, int minRippleSize, int maxRippleSize) {
            this.minRippleSize = Math.min(minRippleSize, maxRippleSize);
            this.maxRippleSize = Math.max(minRippleSize, maxRippleSize);
            this.yOffset = yOffset;
            guideLayout = new GuideLayout(context);
            guideLayout.setOnClickListener(null);
            guideLayout.setOnLongClickListener(null);
        }
    
        /**
         * Before showing action, it must had attached target.
         */
        public void show(@NonNull Activity activity) {
            if (target == null)
                throw new IllegalStateException("You need attach target first.");
    
            View view = activity.findViewById(android.R.id.content);
            if (view instanceof ViewGroup)
                ((ViewGroup) view).addView(guideLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        }
    

    使用示例

        private void showGuidePopupView(final String key) {
            final int showCount = SharePreferencesUtils.getInstance().getInt(key, 0);
            if (showCount >= 3)
                return;
    
            ImageView imageView = new ImageView(this);
            imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            imageView.setImageResource(R.drawable.hand_o_up);
            //
            LinearLayout layout = new LinearLayout(this);
            layout.setOrientation(LinearLayout.VERTICAL);
            layout.setGravity(Gravity.CENTER_HORIZONTAL);
            //
            TextView textView = new TextView(this);
            textView.setTextColor(Color.WHITE);
            textView.setLineSpacing(0, 1.2f);
            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
            textView.setText("点击可查看相关组件运行效果哦!");
            layout.addView(textView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
            //
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            params.topMargin = CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16);
            Button button = new Button(this);
            button.setText("我知道了");
            layout.addView(button, params);
            new GuidePopupView(this)
                    .setMinRippleSize(WindowUtils.getActionBarSize(this))
                    .setMaxRippleSize(WindowUtils.getActionBarSize(this))
                    .setyOffset(WindowUtils.getStatusBarHeight(this) + WindowUtils.getActionBarSize(this))
                    .setBackgroundColor(0xB3000000)
                    .setOnRippleViewUpdateLocationCallback(new GuideLayout.OnRippleViewUpdateLocationCallback() {
                        @Override
                        public void onRippleViewUpdateLocation(@NonNull GuideRippleView rippleView) {
                            //可以根据你自己的需求修改提示动画的相关设置
                            int color = CompatResourceUtils.getColor(rippleView.getContext(), R.color.colorAccent);
                            rippleView.initCirculars(3, new int[]{color, color, color}, 20, .7f);
                        }
                    })
                    .attachTarget(recyclerView.getChildAt(4))
                    .removeAllCustomView()
                    .addCustomView(imageView, new GuideLayout.OnAddCustomViewCallback<ImageView>() {
                        @Override
                        public void onAddCustomView(GuideLayout guideLayout, @Nullable ImageView customView, @NonNull Rect targetRect) {
                            GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                            params.gravity = Gravity.END;
                            params.rightMargin = targetRect.width() / 2 - 10;
                            params.topMargin = targetRect.bottom + 12;
                            guideLayout.addView(customView, params);
    
                            ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
                                    .setDuration(1200);
                            animator.setRepeatCount(-1);
                            animator.start();
                        }
                    })
                    .addCustomView(layout, new GuideLayout.OnAddCustomViewCallback<LinearLayout>() {
                        @Override
                        public void onAddCustomView(GuideLayout guideLayout, @Nullable LinearLayout customView, @NonNull Rect targetRect) {
                            GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                            params.gravity = Gravity.CENTER_HORIZONTAL;
                            params.topMargin = targetRect.bottom + CompatResourceUtils.getDimensionPixelSize(guideLayout, R.dimen.space_64);
                            guideLayout.addView(customView, params);
                        }
                    })
                    .addTargetClickListener(new OnCustomClickListener() {
                        @Override
                        public void onCustomClick(@NonNull View view) {
                            int count = SharePreferencesUtils.getInstance().getInt(key, 0) + 1;
                            SharePreferencesUtils.getInstance().saveInt(key, count);
                            Intent mIntent = new Intent();
                            mIntent.setClass(MainActivity.this, CustomToastActivity.class);
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                                mIntent.putExtra("transition", TransitionEnum.SLIDE.getLabel());
                                startActivity(mIntent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
                            } else {
                                startActivity(mIntent);
                            }
                        }
                    })
                    .addCustomClickListener(button, null, true)
                    .show(this);
    
        }
    

    原理:看小标题就知道了,这是一个PopupWindow。

        /**
         * @param context       context
         * @param yOffset       offset from top
         * @param minRippleSize minimum ripple size
         * @param maxRippleSize max ripple size
         */
        public GuidePopupWindow(Context context, int yOffset, int minRippleSize, int maxRippleSize) {
            this.minRippleSize = Math.min(minRippleSize, maxRippleSize);
            this.maxRippleSize = Math.max(minRippleSize, maxRippleSize);
            this.yOffset = yOffset;
            DisplayMetrics metrics = context.getResources().getDisplayMetrics();
            guideLayout = new GuideLayout(context);
            guideLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    
            mPopupWindow = new PopupWindow();
            mPopupWindow.setContentView(guideLayout);
            mPopupWindow.setWidth(metrics.widthPixels);
            mPopupWindow.setHeight(metrics.heightPixels);
            mPopupWindow.setFocusable(false);
            mPopupWindow.setOutsideTouchable(true);
        }
    

    使用示例:

        private void showGuidePopupWindow(final String key) {
            final int showCount = SharePreferencesUtils.getInstance().getInt(key, 0);
            if (showCount < 3) {
                ImageView imageView = new ImageView(this);
                imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
                imageView.setImageResource(R.drawable.hand_o_up);
                //
                LinearLayout layout = new LinearLayout(this);
                layout.setOrientation(LinearLayout.VERTICAL);
                layout.setGravity(Gravity.CENTER_HORIZONTAL);
                //
                TextView textView = new TextView(this);
                textView.setTextColor(Color.WHITE);
                textView.setLineSpacing(0, 1.2f);
                textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
                textView.setText("点击闪烁按钮可检测\n是否有版本更新哦!");
                layout.addView(textView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
                //
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                params.topMargin = CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16);
                Button button = new Button(this);
                button.setText("我知道了");
                layout.addView(button, params);
    
                guidePopupWindow = new GuidePopupWindow(this)
                        .setMinRippleSize(WindowUtils.getActionBarSize(this))
                        .setMaxRippleSize(WindowUtils.getActionBarSize(this))
                        .setBackgroundColor(0xB3000000)
                        .setOnRippleViewUpdateLocationCallback(new GuideLayout.OnRippleViewUpdateLocationCallback() {
                            @Override
                            public void onRippleViewUpdateLocation(@NonNull GuideRippleView rippleView) {
                                //可以根据你自己的需求修改提示动画的相关设置
                                int color = CompatResourceUtils.getColor(rippleView.getContext(), R.color.colorAccent);
                                rippleView.initCirculars(3, new int[]{color, color, color}, 20, .7f);
                            }
                        })
                        .attachTarget(getActionMenuView())
                        .removeAllCustomView()
                        .addCustomView(imageView, new GuideLayout.OnAddCustomViewCallback<ImageView>() {
                            @Override
                            public void onAddCustomView(GuideLayout guideLayout, @Nullable ImageView customView, @NonNull Rect targetRect) {
                                GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                                params.gravity = Gravity.END;
                                params.rightMargin = targetRect.width() / 2 - 10;
                                params.topMargin = targetRect.bottom + 12;
                                guideLayout.addView(customView, params);
    
                                ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
                                        .setDuration(1200);
                                animator.setRepeatCount(-1);
                                animator.start();
                            }
                        })
                        .addCustomView(layout, new GuideLayout.OnAddCustomViewCallback<LinearLayout>() {
                            @Override
                            public void onAddCustomView(GuideLayout guideLayout, @Nullable LinearLayout customView, @NonNull Rect targetRect) {
                                GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                                params.gravity = Gravity.CENTER;
                                guideLayout.addView(customView, params);
                            }
                        })
                        .addTargetClickListener(new OnCustomClickListener() {
                            @Override
                            public void onCustomClick(@NonNull View view) {
                                int count = SharePreferencesUtils.getInstance().getInt(key, 0) + 1;
                                SharePreferencesUtils.getInstance().saveInt(key, count);
                                loadVersionInfo();
                            }
                        })
                        .addCustomClickListener(button, null, true);
                guidePopupWindow.show();
            }
        }
    

    原理:小标题说明这是一个Dialog。

        /**
         * @param context       context
         * @param yOffset       offset from top
         * @param minRippleSize minimum ripple size
         * @param maxRippleSize max ripple size
         */
        public GuideDialog(Context context, int yOffset, int minRippleSize, int maxRippleSize) {
            super(context);
            supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
            this.minRippleSize = Math.min(minRippleSize, maxRippleSize);
            this.maxRippleSize = Math.max(minRippleSize, maxRippleSize);
            this.yOffset = yOffset;
            guideLayout = new GuideLayout(getContext());
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(guideLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            //设置window背景,默认的背景会有Padding值,不能全屏。当然不一定要是透明,你可以设置其他背景,替换默认的背景即可。
            getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
            //一定要在setContentView之后调用,否则无效
            getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        }
    

    使用示例:

        private void showGuideDialog(final String key) {
            final int showCount = SharePreferencesUtils.getInstance().getInt(key, 0);
            if (showCount >= 3)
                return;
    
            ImageView imageView = new ImageView(this);
            imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            imageView.setImageResource(R.drawable.hand_o_up);
            //
            LinearLayout layout = new LinearLayout(this);
            layout.setOrientation(LinearLayout.VERTICAL);
            layout.setGravity(Gravity.CENTER_HORIZONTAL);
            //
            TextView textView = new TextView(this);
            textView.setTextColor(Color.WHITE);
            textView.setLineSpacing(0, 1.2f);
            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
            textView.setText("点击可查看相关组件运行效果哦!");
            layout.addView(textView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
            //
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            params.topMargin = CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16);
            Button button = new Button(this);
            button.setText("我知道了");
            layout.addView(button, params);
            new GuideDialog(this)
                    .setCanceledOnTouchOutside1(false)
                    .setCancelable1(false)
                    .setMinRippleSize(WindowUtils.getActionBarSize(this))
                    .setMaxRippleSize(WindowUtils.getActionBarSize(this))
    //                .setBackgroundColor(0xB3000000)
                    .setOnRippleViewUpdateLocationCallback(new GuideLayout.OnRippleViewUpdateLocationCallback() {
                        @Override
                        public void onRippleViewUpdateLocation(@NonNull GuideRippleView rippleView) {
                            //可以根据你自己的需求修改提示动画的相关设置
                            int color = CompatResourceUtils.getColor(rippleView.getContext(), R.color.colorAccent);
                            rippleView.initCirculars(3, new int[]{color, color, color}, 20, .7f);
                        }
                    })
                    .attachTarget(recyclerView.getChildAt(4))
                    .removeAllCustomView()
                    .addCustomView(imageView, new GuideLayout.OnAddCustomViewCallback<ImageView>() {
                        @Override
                        public void onAddCustomView(GuideLayout guideLayout, @Nullable ImageView customView, @NonNull Rect targetRect) {
                            GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                            params.gravity = Gravity.END;
                            params.rightMargin = targetRect.width() / 2 - 10;
                            params.topMargin = targetRect.bottom + 12;
                            guideLayout.addView(customView, params);
    
                            ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
                                    .setDuration(1200);
                            animator.setRepeatCount(-1);
                            animator.start();
                        }
                    })
                    .addCustomView(layout, new GuideLayout.OnAddCustomViewCallback<LinearLayout>() {
                        @Override
                        public void onAddCustomView(GuideLayout guideLayout, @Nullable LinearLayout customView, @NonNull Rect targetRect) {
                            GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                            params.gravity = Gravity.CENTER_HORIZONTAL;
                            params.topMargin = targetRect.bottom + CompatResourceUtils.getDimensionPixelSize(guideLayout, R.dimen.space_64);
                            guideLayout.addView(customView, params);
                        }
                    })
                    .addTargetClickListener(new OnCustomClickListener() {
                        @Override
                        public void onCustomClick(@NonNull View view) {
                            int count = SharePreferencesUtils.getInstance().getInt(key, 0) + 1;
                            SharePreferencesUtils.getInstance().saveInt(key, count);
                            Intent mIntent = new Intent();
                            mIntent.setClass(MainActivity.this, CustomToastActivity.class);
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                                mIntent.putExtra("transition", TransitionEnum.SLIDE.getLabel());
                                startActivity(mIntent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
                            } else {
                                startActivity(mIntent);
                            }
                        }
                    })
                    .addCustomClickListener(button, null, true)
                    .show();
    
        }
    
    • 4、WindowManager添加View方式WindowManager
      此种方式有很多坑,不建议用此方式。

    原理:
    WindowManager manager = activity.getWindowManager();
    manager.addView(View view, ViewGroup.LayoutParams params);

      童鞋们给个💕吧!用起来也很简单!童鞋们不要被我一大堆示例代码吓着了。

    篇尾

      Wechat:eoy9527

    在人类历史的长河中,真理因为像黄金一样重,总是沉于河底而很难被人发现,相反地,那些牛粪一样轻的谬误倒漂浮在上面到处泛滥。 —— 培根

    相关文章

      网友评论

        本文标题:Android 强大实用的功能引导组件

        本文链接:https://www.haomeiwen.com/subject/huapvftx.html