Android开发技巧——高亮的用户操作指南


2015-12-15补记:

发现使用PopupWindow进行遮罩层的显示,在华为P7上会有问题。具体表现为:画出来的高亮部分会偏下。原因为:通过view.getRootView获取到DecorView,把其作为PopupWindow的anchorView来显示,然而在华为P7上依然是显示在status bar下面,而我们计算高亮时获取view的高度,是从decorView开始计算的,导致之间的距离相差了一个状态栏的高度。参考张鸿洋大神的做法对实现进行了修改,相关修改见文末。


一不小心成了博客之星的候选人,还有许多朋友帮我投票,无以回报,只好发篇博客以表谢意。

前面四篇写了关于自定义控件的一些基础知识。在我的理解中,其实做Android开发久了,在项目领域无非是更熟悉业务流程,而在Android的技术领域,基本上是走向两个方向,或是两个方向都走。

一是做界面上的开发,比如各种下拉刷新,酷炫的对话框,各种动效等,这其中有的是为了界面的更好实现,有些是为了设计的更好表现。

第二个方向则是非界面上的开发,比如热修复,事件总线,网络请求库,图片加载库等,侧重于解决某一层面的问题,提高效率,降低风险,或是其他原因。

我自己目前对第二个方向所涉较少,而对于第一个方向,接触比较多的就是自定义控件了。除了与属性相关的设置以及控件本身的方法的调用,自定义控件的表现主要还是在onDraw里的绘制,而onDraw里的绘制,基本上也是你能想到多远,加上所需的数学知识和相关API,就能做到多远。

需求

说得有点远了,这一篇主要分享的是操作指南的一种实现,聚光灯高亮某个控件。



如图所示,所谓高亮,其实只是把屏幕全屏用半透明的遮罩层挡住,而只留下其中某一控件不遮住,并在控件的附近加上具体的文字提示,而让用户的焦点都聚集到这一控件上,并看到提示的文字。然后点击圆圈里面的控件,有对应的响应事件。按返回键不能退出这个界面。

上面的例子还是相对比较好实现的,因为它没有QQ的提示里面的箭头,只是一个圆,并且在下面一行文字。本文也只讨论这种实现,有其他需求的可以自己扩展。

思路

全屏遮罩(不包括显示的系统状态栏),首先我所想到的就是PopupWindow了。在PopupWindow中显示一个ViewView的背景为半透明黑色,并且中间抠出一个圆,然后下面再绘制一行文字就可以了。抠出一个圆的实现可以通过对Paint调用setXfermodesetXfermode的参数可以参考《setXfermode》这篇博客。

那么思路如下:

1. 继承View,在onDraw(Canvas canvas)方法中绘制一层半透明背景

2. 设置setXfermode(xfermode)为PorterDuff.Mode.DST_OUT,计算圆所在坐标,并画出

3. 在圆的下方画出文字

4. 提供接口showTipForView(final View view, String tip, OnClickListener listener)供外部调用,参数为:需要高亮的控件,文字提示,点击之后的回调。

5. 计算需要响应的点击区域,回调listener。

实现

下面是具体的实现过程。

聚光灯高亮

首先先实现高亮的效果,关于点击下一步再说。从上面的分析我们可以知道至少需要以下变量:

public class HighLightView extends View {
    private static final float RADIUS_RATIO = 1f / 3;
    private static final PorterDuffXfermode MODE_DST_OUT = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);

    private final Paint mPaint;

    private int mOverlayColor;//遮罩层颜色
    private int mRadius; // 圆半径
    private int mCenterX; // 圆心横坐标
    private int mCenterY; // 圆心纵坐标

    private String mTip; // 提示文字
    private float mTipX; // 文字横坐标
    private float mTipY; // 文字纵坐标

    private View mAnchorView; // 高亮的View
    private final PopupWindow mPopupWindow;
}

然后在构造方法中,对mPopupWindow和mPaint进行初始化。由于我这边遮罩颜色是固定的,所以我也在这边一起初始化了。代码如下:


    public HighLightView(Context context) {
        super(context);
        mPopupWindow = new PopupWindow(this, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, true);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.FILL);
        int textSize = getResources().getDimensionPixelSize(R.dimen.text_32pt);
        mPaint.setTextSize(textSize);
        mPaint.setColor(Color.WHITE);
        mOverlayColor = getResources().getColor(R.color.guide_overlay);
    }

然后我们实现在onDraw(Canvas canvas)里的绘制,绘制代码很简单,画个遮罩层,设置Xfermode,画个圆,去掉Xfermode,画个文字。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(mOverlayColor);
        mPaint.setXfermode(MODE_DST_OUT);
        canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint);
        mPaint.setXfermode(null);
        canvas.drawText(mTip, mTipX, mTipY, mPaint);
    }

但是这里的坐标都需要在调用showTipForView(final View view, String tip, OnClickListener listener)了之后进行计算。所以下面是这个方法的实现过程:

   /**
     * @param view     要高亮的view
     * @param tip      文字提示
     * @param listener 点击回调
     */
    public void showTipForView(final View view, final String tip, OnClickListener listener) {
        mAnchorView = view;
        mTip = tip;
        mClickListener = listener;

        final View rootView = view.getRootView();

        final int[] location = new int[2];
        view.getLocationInWindow(location);//获取当前view在窗口中的位置
        final int viewHeight = view.getHeight();
        final int viewWidth = view.getWidth();
        mRadius = (int) (viewWidth * RADIUS_RATIO);
        mCenterX = viewWidth / 2 + location[0];// 获取圆心x坐标
        mCenterY = viewHeight / 2 + location[1]; // 获取圆心y坐标

        mTipX = (rootView.getWidth() - mPaint.measureText(mTip)) / 2; //提示文字的横坐标,居中即可
        mTipY = location[1] + mRadius * 2; // 提示文字的纵坐标,只需要在圆的下方,这里设为view的纵坐标加上直径
        mPopupWindow.showAtLocation(rootView, Gravity.TOP, 0, 0);
    }

实现过程很简单,获取view的坐标,宽,高,然后进行各种计算并赋给其他参数。

但是在运行的时候,你会发现,如果这时候这个view还没有被绘制出来的话,可能出现两个问题:

1,坐标计算不正确,圆都跑左上角的屏幕边缘去了。

2,mPopupWindow.showAtLocation调用的时候报异常:

android.view.WindowManager$BadTokenException
Unable to add window -- token null is not valid; is your activity running?

第一个的原因是view还没有绘制完成,所以获取到的坐标不是最后的坐标。第二个的原因是我们调用的时候Activity的生命周期还没完成。通常我们都会在某个界面刚显示的时候弹出这种提示,所以会存在这样的问题。

stackoverflow了之后,发现问题的解决方法很简单,只需要在view中调用 post方法,把计算和弹出的代码放在这里面就可以了,它会在窗口显示之后被调用,见《Problems creating a Popup Window in Android Activity》。这个答案不但解决了问题2,也顺便解决了问题1 。

所以代码改为:

    /**
     * @param view     要高亮的view
     * @param tip      文字提示
     * @param listener 点击回调
     */
    public void showTipForView(final View view, final String tip, OnClickListener listener) {
        mAnchorView = view;
        mTip = tip;
        mClickListener = listener;

        view.post(new Runnable() {
            @Override
            public void run() {
                final View rootView = view.getRootView();

                final int[] location = new int[2];
                view.getLocationInWindow(location);//获取当前view在窗口中的位置
                final int viewHeight = view.getHeight();
                final int viewWidth = view.getWidth();
                mRadius = (int) (viewWidth * RADIUS_RATIO);
                mCenterX = viewWidth / 2 + location[0];// 获取圆心x坐标
                mCenterY = viewHeight / 2 + location[1]; // 获取圆心y坐标

                mTipX = (rootView.getWidth() - mPaint.measureText(mTip)) / 2; //提示文字的横坐标,居中即可
                mTipY = location[1] + mRadius * 2; // 提示文字的纵坐标,只需要在圆的下方,这里设为view的纵坐标加上直径
                mPopupWindow.showAtLocation(rootView, Gravity.TOP, 0, 0);
            }
        });
    }

点击事件

这里我们期望的点击区域是圆和按钮的相交部分。但实际上,我们不需要这么精确,只需要确定与它相近的一个矩形就可以了,如下图所示:



在这里,我们假定可点击区域为与宽平等的直径在这个View上平移所经过的面积,它与实际上圆与View的相交部分只差4个小角,在手指触摸的情况下可以忽略这个误差。

相关的变量定义如下:

    /**
     * 可点击的区域
     */
    private Rect mClickRect;
    /**
     * 是否在点击区域按下
     */
    private boolean mDownInClickRect;

在前面计算其他变量的方法中,再加上一行代码,用于计算这个矩形:

    // 可点击区域为圆心按钮相交的近似矩形
    mClickRect = new Rect(mCenterX - mRadius, location[1], mCenterX + mRadius, location[1] + viewHeight);

接着处理点击事件,重写onTouchEvent(MotionEvent event)即可,判断按下和松开时是否都在点击范围内,是的话就回调,并且把mPopupWindow隐藏掉。代码如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                final PointF down = new PointF(event.getX(), event.getY());
                if (isInClickRect(down)) {
                    mDownInClickRect = true;
                }
                break;

            case MotionEvent.ACTION_UP:
                if (!mDownInClickRect) {
                    break;
                }
                final PointF up = new PointF(event.getX(), event.getY());
                if (isInClickRect(up) && mClickListener != null) {
                    mClickListener.onClick(mAnchorView);
                    mPopupWindow.dismiss();
                }
                mDownInClickRect = false;
                return true;
        }
        return super.onTouchEvent(event);
    }

    private boolean isInClickRect(PointF point) {
        return point.x > mClickRect.left && point.x < mClickRect.right
                && point.y > mClickRect.top && point.y < mClickRect.bottom;
    }

扩展讨论

如果需要把这个高亮的控件做得通用,我们需要往外再提供一些接口用于设置一些内容,比如:

1,遮罩层的颜色

2,文字的样式

3,高亮的样式(我这里的项目中是显示一个圈,但更多的可能是高亮控件本身)

4,其他需要显示的内容,比如箭头,或者是其他图形或控件(类似于“我知道了”的按钮)

因项目较忙,写博客的时间不多,如果有好的建议,优化,或者我没有考虑到的场景,欢迎评论及指正。

2015-12-15补述:

上面使用PopupWindow来实现的原理貌似无法解决部分机型的兼容问题,导致绘制出来的高亮部分偏下,参考hongyangAndroid/Highlight的实现过程,进行以下四步修改:

1,去掉PopupWindow,显示时改为直接添加到DecorView的方式:

    if(anchorView instanceof ViewGroup) {
        ((ViewGroup)anchorView).addView(HighLightView.this, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    }

2,要隐藏的时候,将其remove掉:

    ((ViewGroup)getParent()).removeView(HighLightView.this);

3,这个时候View的事件会穿透到下一层去,所以需要在构造方法里进行设置:

    setFilterTouchesWhenObscured(false);

4,原先绘制圆的代码,此时只会绘制出一个大黑块,所以需要一个Bitmap变量,用于保存你要显示的内容,包括遮罩层,高亮的圆,文字,如下:

        mBitmap = Bitmap.createBitmap(anchorView.getWidth(), anchorView.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mBitmap);
        canvas.drawColor(mOverlayColor);
        mPaint.setXfermode(X_FER_MODE);
        canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint);
        mPaint.setXfermode(null);
        mPaint.setColor(Color.WHITE);
        canvas.drawText(mTip, mTipX, mTipY, mPaint);

该类完整的代码如下:

package com.parkingwang.app.guide.operation;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.parkingwang.app.R;

/**
 * 高亮的View。
 *
 * @author Geek_Soledad (msdx.android@qq.com)
 * @version 2015-12-07 3.1
 * @since 2015-12-07 3.1
 */
public class HighLightView extends View {
    private static final float RADIUS_RATIO = 1f / 3;
    private static final PorterDuffXfermode X_FER_MODE = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);

    private final Paint mPaint;

    private int mOverlayColor;//遮罩层颜色
    private int mRadius; // 圆半径
    private int mCenterX; // 圆心横坐标
    private int mCenterY; // 圆心纵坐标

    private String mTip; // 提示文字
    private float mTipX; // 文字横坐标
    private float mTipY; // 文字纵坐标

    private View mHighLightView; // 高亮的View
    private Bitmap mBitmap;

    private OnClickListener mClickListener; // 点击回调
    /**
     * 可点击的区域
     */
    private Rect mClickRect = new Rect();
    /**
     * 是否在点击区域按下
     */
    private boolean mDownInClickRect;

    public HighLightView(Context context) {
        super(context);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.FILL);
        int textSize = getResources().getDimensionPixelSize(R.dimen.text_32pt);
        mPaint.setTextSize(textSize);
        mOverlayColor = getResources().getColor(R.color.guide_overlay);

        setFilterTouchesWhenObscured(false);
    }

    /**
     * @param view     要高亮的view
     * @param tip      文字提示
     * @param listener 点击回调
     */
    public void showTipForView(final View view, final String tip, OnClickListener listener) {
        mHighLightView = view;
        mTip = tip;
        mClickListener = listener;

        view.post(new Runnable() {
            @Override
            public void run() {
                final View anchorView = view.getRootView();
                prepare(anchorView, view);
                if(anchorView instanceof ViewGroup) {
                    ((ViewGroup)anchorView).addView(HighLightView.this, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
                }
            }
        });
    }

    private void prepare(View anchorView, View view) {
        final int[] baseLocation = new int[2];
        anchorView.getLocationInWindow(baseLocation);
        final int[] viewLocation = new int[2];
        view.getLocationInWindow(viewLocation);//获取当前view在窗口中的位置
        final int viewHeight = view.getHeight();
        final int viewWidth = view.getWidth();
        final int halfHeight = viewHeight / 2;
        mRadius = (int) (viewWidth * RADIUS_RATIO);
        mCenterX = viewLocation[0] - baseLocation[0] + viewWidth / 2;// 获取圆心x坐标
        mCenterY = viewLocation[1] - baseLocation[1] + halfHeight; // 获取圆心y坐标

        // 可点击区域为圆心按钮相交的近似矩形
        mClickRect.set(mCenterX - mRadius, mCenterY - halfHeight, mCenterX + mRadius, mCenterY + halfHeight);
        mTipX = (anchorView.getWidth() - mPaint.measureText(mTip)) / 2; //提示文字的横坐标,居中即可
        mTipY = viewLocation[1] + mRadius * 2; // 提示文字的纵坐标,只需要在圆的下方,这里设为view的纵坐标加上直径
        mBitmap = Bitmap.createBitmap(anchorView.getWidth(), anchorView.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mBitmap);
        canvas.drawColor(mOverlayColor);
        mPaint.setXfermode(X_FER_MODE);
        canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint);
        mPaint.setXfermode(null);
        mPaint.setColor(Color.WHITE);
        canvas.drawText(mTip, mTipX, mTipY, mPaint);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(mBitmap, 0, 0, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                final PointF down = new PointF(event.getX(), event.getY());
                if (isInClickRect(down)) {
                    mDownInClickRect = true;
                }
                break;

            case MotionEvent.ACTION_UP:
                if (!mDownInClickRect) {
                    break;
                }
                final PointF up = new PointF(event.getX(), event.getY());
                if (isInClickRect(up) && mClickListener != null) {
                    mClickListener.onClick(mHighLightView);
                    ((ViewGroup)getParent()).removeView(HighLightView.this);
                }
                mDownInClickRect = false;
                return true;
        }
        return true;
    }

    private boolean isInClickRect(PointF point) {
        return point.x > mClickRect.left && point.x < mClickRect.right
                && point.y > mClickRect.top && point.y < mClickRect.bottom;
    }
}

参考资料:

《setXfermode》:http://407827531.iteye.com/blog/1470519

《Problems creating a Popup Window in Android Activity》:http://stackoverflow.com/questions/4187673/problems-creating-a-popup-window-in-android-activity/4713487#4713487

Highlight:https://github.com/hongyangAndroid/Highlight

本文原创,转载请注明出处:http://blog.csdn.net/maosidiaoxian/article/details/50248145

Android开发技巧——高亮的用户操作指南的更多相关文章

  1. 50个android开发技巧

    50个android开发技巧 http://blog.csdn.net/column/details/androidhacks.html

  2. Android开发技巧——大图裁剪

    本篇内容是接上篇<Android开发技巧--定制仿微信图片裁剪控件> 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪. 裁剪控件的简单使 ...

  3. Android开发技巧——使用PopupWindow实现弹出菜单

    在本文当中,我将会与大家分享一个封装了PopupWindow实现弹出菜单的类,并说明它的实现与使用. 因对界面的需求,android原生的弹出菜单已不能满足我们的需求,自定义菜单成了我们的唯一选择,在 ...

  4. Android开发技巧——实现可复用的ActionSheet菜单

    在上一篇<Android开发技巧--使用Dialog实现仿QQ的ActionSheet菜单>中,讲了这种菜单的实现过程,接下来将把它改成一个可复用的控件库. 本文原创,转载请注明出处: h ...

  5. Android开发技巧——自定义控件之增加状态

    Android开发技巧--自定义控件之增加状态 题外话 这篇本该是上周四或上周五写的,无奈太久没写博客,前几段把我的兴头都用完了,就一拖再拖,直到今天.不想把这篇拖到下个月,所以还是先硬着头皮写了. ...

  6. Android开发技巧——自定义控件之使用style

    Android开发技巧--自定义控件之使用style 回顾 在上一篇<Android开发技巧--自定义控件之自定义属性>中,我讲到了如何定义属性以及在自定义控件中获取这些属性的值,也提到了 ...

  7. Android开发技巧——自定义控件之自定义属性

    Android开发技巧--自定义控件之自定义属性 掌握自定义控件是很重要的,因为通过自定义控件,能够:解决UI问题,优化布局性能,简化布局代码. 上一篇讲了如何通过xml把几个控件组织起来,并继承某个 ...

  8. Android开发技巧——自定义控件之组合控件

    Android开发技巧--自定义控件之组合控件 我准备在接下来一段时间,写一系列有关Android自定义控件的博客,包括如何进行各种自定义,并分享一下我所知道的其中的技巧,注意点等. 还是那句老话,尽 ...

  9. Android开发技巧——写一个StepView

    在我们的应用开发中,有些业务流程会涉及到多个步骤,或者是多个状态的转化,因此,会需要有相关的设计来展示该业务流程.比如<停车王>应用里的添加车牌的步骤. 通常,我们会把这类控件称为&quo ...

随机推荐

  1. 18 UI美化状态集合的位图selector

    当我们某个控件 想在不同状态下显示不同的背景图的需求 如我们需要按钮在正常状态显示一种图 按下显示另一背景图 或者单选框被选中时是一种显示图片 没选中是另一种背景图 例子 按钮在不同状态显示不同的背景 ...

  2. UNIX网络编程——UNIX域套接字编程和socketpair 函数

    一.UNIX Domain Socket IPC socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket.虽然网络soc ...

  3. Java并发框架——公平性

    所谓公平性指所有线程对临界资源申请访问权限的成功率都一样,不会让某些线程拥有优先权.通过前面的CLH Node FIFO学习知道了等待队列是一个先进先出的队列,那么是否就可以说每条线程获取锁时就是公平 ...

  4. Android之Gallery和Spinner-Android学习之旅(二十九)

    Spinner简介 spinner是竖直方向展开一个列表供选择.和gallery都是继承了AbsSpinner,AbsSpinner继承了AdapterView,因此AdaptyerView的属性都可 ...

  5. iOS中 最新支付宝支付(AliPay) 韩俊强的博客

    每日更新关注:http://weibo.com/hanjunqiang  新浪微博 现在的支付方式一般有三种, 支付宝, 微信, 网银. 个人觉得最简单易用的还是支付宝, 微信虽然看起来币支付宝要简单 ...

  6. 03安卓TextView

    一  TextView    父类 : View     >概念:文本控件 :文本内容的显示   默认配置不可编辑  子类EditText可以编辑 *********************** ...

  7. 【嵌入式开发】 ARM 汇编 (指令分类 | 伪指令 | 协处理器访问指令)

    作者 : 韩曙亮 博客地址 : http://blog.csdn.net/shulianghan/article/details/42408137 转载请著名出处 本博客相关文档下载 :  -- AR ...

  8. 偏置方差分解Bias-variance Decomposition

    http://blog.csdn.net/pipisorry/article/details/50638749 偏置-方差分解(Bias-Variance Decomposition) 偏置-方差分解 ...

  9. Linux的sort命令

     Linux的sort命令 Linux的sort命令就是一种对文件排序的工具,sort命令的功能十分强大,是Shell脚本编程时常使用的文件排序工具. sort命令将输入文件看做由多条记录组成的数据流 ...

  10. Java Web 高性能开发,第 3 部分: 网站优化实战

    这个系列的前两篇,介绍了前端的优化技术,这些技术秉承了前人至高无上的智慧,我只是负责吸收和传播.然而,这些技术一般也都是某某大型网站的技术经验,我们大部分人或许只能接触到相对小规模的网站,小规模的网站 ...