亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

Android 強大實用的功能引導組件

標簽:
Android

开篇

  每一个新上线App的功能引导很重要,没有功能引导,我们往往需要花费大量的时间以及人力去培训客户,从公司层面上这无疑是增加了很大的开销。
  利用闲暇时间封装了一个功能引导组件,使用方便。次组件已经发布到jCenter。希望童鞋们多多支持!

功能盘点

  • 使用简洁方便

  • 带有引导波纹动画(可个性化配置,如颜色、动画速度、大小等)

  • 引导画面的上按钮与需被引导的按钮同等样式(如大小、颜色、形状、内容...)

  • 支持动态添加各种需要显示的view.

效果截屏

webp

GuidanceRippleView

webp

GuidanceLayout

立即体验

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

webp


[开源库传送门:https://github.com/JustinRoom/GuidanceDemo)


简析源码

下面依次分享相关组件

一、GuidanceRippleView

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

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

  • protected void onDraw(Canvas canvas)

    @Override
    protected void onDraw(Canvas canvas) {        if (speed <= 0) {
            speed = space / frameCountPerSecond;
        }        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) (0xFF * (1 - radius / maxRadius) + .5f);        for (int i = 0; i < count; i++) {
            paint.setColor(colors[i]);
            paint.setAlpha(alpha);            float tempRadius = radius - space * i;            if (tempRadius > 0)
                canvas.drawCircle(maxRadius, maxRadius, tempRadius, paint);
        }
        radius += speed;        if (radius > maxRadius)
            radius = 0;        if (isRunning)
            invalidate();
    }

控制动画相关方法:

    public void start() {        if (isRunning)            return;
        radius = 0;
        isRunning = true;
        invalidate();
    }    public void stop() {
        radius = 0;
        isRunning = false;
        invalidate();
    }    public void pause() {
        isRunning = false;
    }    public void resume() {
        isRunning = true;
        invalidate();
    }
二、GuidanceLayout

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

分析关键code:

    /**
     * Update the target view's location.
     *
     * @param targetView         target
     * @param l                  the left margin
     * @param t                  the top margin
     * @param rippleViewSize     the size of {@link #rippleViewView}
     * @param rippleClipToTarget true, clip {@link #rippleViewView} to {@link #targetRect} area.
     */
    public void updateTargetViewLocation(@NonNull View targetView, int l, int t, int rippleViewSize, boolean rippleClipToTarget, OnRippleViewLocationUpdatedCallback callback) {
        Bitmap bitmap = ViewDrawingCacheUtils.getDrawingCache(targetView);
        updateTargetViewLocation(bitmap, l, t, rippleViewSize, rippleClipToTarget, callback);
    }    /**
     * Update the target view's location.
     *
     * @param targetView         target
     * @param l                  the left margin
     * @param t                  the top margin
     * @param listener           listener for initializing {@link #rippleViewView}'s size
     * @param rippleClipToTarget true, clip {@link #rippleViewView} to {@link #targetRect} area.
     */
    public void updateTargetViewLocation(@NonNull View targetView, int l, int t, OnInitRippleViewSizeListener listener, boolean rippleClipToTarget, OnRippleViewLocationUpdatedCallback callback) {
        Bitmap bitmap = ViewDrawingCacheUtils.getDrawingCache(targetView);        int size = listener == null ? getResources().getDimensionPixelSize(R.dimen.guidance_default_ripple_size) : listener.onInitializeRippleViewSize(bitmap);
        updateTargetViewLocation(bitmap, l, t, size, rippleClipToTarget, callback);
    }    public void updateTargetViewLocation(Bitmap bitmap, int l, int t, int rippleViewSize, boolean rippleClipToTarget, OnRippleViewLocationUpdatedCallback callback) {
        curStepIndex++;        if (bitmap == null)            return;
        targetRect.set(l, t, l + bitmap.getWidth(), t + bitmap.getHeight());
        ViewGroup.LayoutParams params = targetView.getLayoutParams();
        params.width = targetRect.width();
        params.height = targetRect.height();        if (params instanceof MarginLayoutParams) {
            ((MarginLayoutParams) params).leftMargin = targetRect.left;
            ((MarginLayoutParams) params).topMargin = targetRect.top;
        }
        targetView.setLayoutParams(params);
        targetView.setImageBitmap(bitmap);
        updateRippleViewLocation(rippleViewSize, callback);        if (rippleClipToTarget)
            rippleViewView.setClip(targetRect.width(), targetRect.height());        else
            rippleViewView.setClip(-1, -1);
    }    /**
     * Update ripple view's location.
     *
     * @param size     size
     * @param callback call back when the ripple view's location was updated.
     */
    private void updateRippleViewLocation(int size, OnRippleViewLocationUpdatedCallback callback) {        if (size < 0)            throw new IllegalArgumentException("Bad params:size is less than zero.");
        ViewGroup.LayoutParams params = rippleViewView.getLayoutParams();
        params.width = size;
        params.height = size;        if (params instanceof MarginLayoutParams) {
            ((MarginLayoutParams) params).leftMargin = (targetRect.left + targetRect.right - size) / 2;
            ((MarginLayoutParams) params).topMargin = (targetRect.top + targetRect.bottom - size) / 2;
        }
        rippleViewView.setLayoutParams(params);        if (callback != null)
            callback.onRippleViewLocationUpdated(rippleViewView, targetRect);
    }

targetView——需要被引导的view
详细逻辑步骤:

  • 1、获取targetView在屏幕中的坐标位置

  • 2、获取targetViewdrawingCache

方法一:获取targetViewdrawingCache

    public static Bitmap getDrawingCache(@NonNull View view) {
        view.setDrawingCacheEnabled(true);
        Bitmap bitmap = view.getDrawingCache();
        view.setDrawingCacheEnabled(false);        return bitmap;
    }

方法二:让targetView在我们自己创建的画布上画一遍。

    public static Bitmap getDrawingCache(@NonNull View view) {        int width = view.getWidth();        int height = view.getHeight();        if (width + height == 0) {
            view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            width = view.getMeasuredWidth();
            height = view.getMeasuredHeight();
        }
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        view.draw(canvas);        return bitmap;
    }
  • 3、新建一个ImageView并加载第2部中获取到的Bitmap

  • 4、添加水波纹动画view。

三、3种展示功能引导方式

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

    public GuidancePopupWindow(@NonNull Activity activity) {        this(activity, 0x99000000);
    }    public GuidancePopupWindow(@NonNull Activity activity, @ColorInt int backgroundColor) {        this.activity = activity;
        guidanceLayout = new GuidanceLayout(activity);
        guidanceLayout.setId(R.id.guidance_default_layout_id);
        guidanceLayout.setBackgroundColor(backgroundColor);
        guidanceLayout.setTargetClickListener(new View.OnClickListener() {            @Override
            public void onClick(View v) {                if (listener == null || !listener.onTargetClick(guidanceLayout))
                    dismiss();
            }
        });
    }    public void show() {
        show(SHOW_IN_CONTENT);
    }    public void show(@ShowType int showType) {        this.curShowType = showType;        switch (curShowType) {            case SHOW_IN_CONTENT:                //找到根布局中id为android.R.id.content的ViewGroup
                //添加GuidanceLayout控件
                FrameLayout contentLayout = activity.findViewById(android.R.id.content);                if (!isGuidanceLayoutAdded(guidanceLayout)) {
                    contentLayout.addView(guidanceLayout);
                }                break;            case SHOW_IN_WINDOW:                //此模式下很多权限方面的坑,不建议使用
                WindowManager.LayoutParams params = new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
                } else {
                    params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
                }
                activity.getWindow().getWindowManager().addView(guidanceLayout, params);                break;
        }
    }

TYPE_APPLICATION_OVERLAYTYPE_SYSTEM_ALERT需要android.permission.SYSTEM_ALERT_WINDOW权限,在6.0系统及以下,只要在AndroidManifest.xml文件中声名即可;在6.0以上系统中,我们需要主动申请权限,申请方法如下:

    public static boolean checkOverlayPermission(@NonNull FragmentActivity activity, int requestCode){        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)            return true;        if (!Settings.canDrawOverlays(activity)) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            intent.setData(Uri.parse("package:" + activity.getPackageName()));
            activity.startActivityForResult(intent, requestCode);
        }        return true;
    }

使用示例

    private void showContentGuidance() {        final GuidancePopupWindow popupWindow = new GuidancePopupWindow(getActivity());
        popupWindow.setTargetClickListener(new OnTargetClickListener() {            @Override
            public boolean onTargetClick(GuidanceLayout layout) {
                Toast.makeText(layout.getContext(), "clicked me", Toast.LENGTH_SHORT).show();                switch (layout.getCurStepIndex()) {                    case 0:
                        layout.removeAllCustomViews();
                        showStep(layout, R.id.item_layout_1);                        return true;                    case 1:
                        layout.removeAllCustomViews();
                        showStep(layout, R.id.item_layout_2);                        return true;                    case 2:
                        layout.removeAllCustomViews();
                        showStep(layout, R.id.item_layout_3);                        return true;                    default:                        return false;
                }
            }
        });
        popupWindow.show();
        GuidanceLayout guidanceLayout = popupWindow.getGuidanceLayout();
        showStep(guidanceLayout, R.id.item_layout_0);
    }

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

        super(context);
        setCancelable(false);
        setCanceledOnTouchOutside(false);
    }    public GuidanceDialog(@NonNull Context context, int themeResId) {        super(context, themeResId);
        setCancelable(false);
        setCanceledOnTouchOutside(false);
    }    @Override
    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
        guidanceLayout = new GuidanceLayout(getContext());
        guidanceLayout.setTargetClickListener(new View.OnClickListener() {            @Override
            public void onClick(View v) {                if (listener == null || !listener.onTargetClick(guidanceLayout))
                    dismiss();
            }
        });
        setContentView(guidanceLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));        if (getWindow() != null) {            //设置window背景,默认的背景会有Padding值,不能全屏。当然不一定要是透明,你可以设置其他背景,替换默认的背景即可。
            getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));            //一定要在setContentView之后调用,否则无效
            getWindow().setLayout(width, height);
        }
    }

使用示例:

    private void showGuidanceDialog() {        final GuidanceDialog dialog = new GuidanceDialog(getContext());
        dialog.setTargetClickListener(new OnTargetClickListener() {            @Override
            public boolean onTargetClick(GuidanceLayout layout) {
                Toast.makeText(layout.getContext(), "clicked me", Toast.LENGTH_SHORT).show();                switch (layout.getCurStepIndex()) {                    case 0:
                        showStep(layout, R.id.item_layout_1);                        return true;                    case 1:
                        showStep(layout, R.id.item_layout_2);                        return true;                    case 2:
                        showStep(layout, R.id.item_layout_3);                        return true;                    default:                        return false;
                }
            }
        });
        dialog.show();
        GuidanceLayout guidanceLayout = dialog.getGuidanceLayout();        if (guidanceLayout == null)            return;
        showStep(guidanceLayout, R.id.item_layout_0);
    }

公共方法:

    private void showStep(GuidanceLayout layout, int targetViewId) {
        layout.removeAllCustomViews();
        showStep(layout, getView().findViewById(targetViewId));
    }    private void showStep(GuidanceLayout guidanceLayout, View target) {
        Context context = guidanceLayout.getContext();        int statusBarHeight = ViewDrawingCacheUtils.getStatusBarHeight(context);        int actionBarHeight = ViewDrawingCacheUtils.getActionBarSize(context);        int[] location = ViewDrawingCacheUtils.getWindowLocation(target);
        guidanceLayout.updateTargetViewLocation(
                target, location[0],
                location[1] - statusBarHeight,                new GuidanceLayout.OnInitRippleViewSizeListener() {                    @Override
                    public int onInitializeRippleViewSize(@NonNull Bitmap bitmap) {                        return bitmap.getHeight();
                    }
                },                true,                new GuidanceLayout.OnRippleViewLocationUpdatedCallback() {                    @Override
                    public void onRippleViewLocationUpdated(@NonNull GuidanceRippleView rippleView, @NonNull Rect targetRect) {

                    }
                });

        ImageView imageView = new ImageView(guidanceLayout.getContext());
        imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
        imageView.setImageResource(R.drawable.hand_o_up);
        guidanceLayout.addCustomView(imageView, new GuidanceLayout.OnCustomViewAddListener<ImageView>() {            @Override
            public void onViewInit(@NonNull ImageView customView, @NonNull FrameLayout.LayoutParams params, @NonNull Rect targetRect) {
                customView.measure(0, 0);
                params.topMargin = targetRect.bottom + 12;
                params.leftMargin = targetRect.left - (customView.getMeasuredWidth() - targetRect.width()) / 2;
            }            @Override
            public void onViewAdded(@NonNull ImageView customView, @NonNull Rect targetRect) {
                ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
                        .setDuration(1200);
                animator.setRepeatCount(-1);
                animator.start();
            }
        }, null);
    }
  • 3、WindowManager添加View方式WindowManager
    此种方式有很多坑,不建议用此方式。

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



作者:JustinRoom
链接:https://www.jianshu.com/p/c1aaddd93245


點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消