Android自定义TipView

自定义view--TipView

TipView其实就是类似QQ长按消息弹出来的横放的提示框。
通过看书和参考各位大神的博客(再次对大神表示恭敬),我用了一下午时间写完了这么一个view。
先来看图:


image.png
image.png

image.png

1 自定义TipView思路

1 首先我们考虑是继承View还是ViewGroup
其实TipView直观看更像是一个group,里面有子view。但其实我们并不需要继承ViewGroup,因为我们不用像LinearLayout那样在布局文件里面去添加子view,而且TipView的item我们用文字就好。如果继承于Group我们还要考虑onLayout的问题,为了简单我直接继承自View。
2 重写方法
TipView要像PopupWindow、Dialog一样显示在Activity上而不是添加到父容器中,原因是如果创建后添加到父容器中去托管的话,父容器的布局规则会影响我们TipView的显示效果。所以我们要使用WindowManager来把TipView添加到外层布局,并且要充满屏幕,i原因为我们要点击tem之外的地方使TipView消失。所以view大小是固定充满屏幕的,不需要重写onMeasure。
需要重写onDraw来绘制view。
3 显示位置
TipView主要分两部分,一部分是三角标,一部分是带有圆角的主体。
当我们点击后,三角标顶点始终在点击位置上方一定距离(如果顶点定位在点击位置,会导致手指挡住一部分三角,用户体验度不佳),并且主体不要与屏幕左右边界碰撞,当要遮挡ToolBar时向下绘制。

2 定义变量

public static final int TOP = 0;//从点击位置上面绘制
    public static final int DOWN = 1;//...下面...

    private int mItemWidth;//item宽
    private int mItemHeight;//item高
    private int mTriaHeight;//三角的高度
    private int mHalfTriaWidth;//三角的半宽
    private int mTriaAcme;//三角的顶点
    private int mTriaItemBorder;//三角的顶点
    private int realLeft;//窗口距左边的值
    private int marginSide;//窗口距左右边的值,防止出现的窗口紧贴边界
    private int mSeparateLineColor = Color.WHITE;
    private int mTextSize;//选项文字的大小
    private int mTextColor;//选项文字的颜色

    private int mItemSeparation;//分割线宽度;
    private int mRadius;//圆角
    private List<TextItem> items;//存放item的集合
    private List<Rect> mItemRectList = new ArrayList<>(); // 存储每个方块
    private Paint mPaint;//画笔
    private Paint mSeparationPaint;//分割线画笔
    private Paint mSPaint;//三角的画笔
    private Path mPath;//路径
    private int x, y;//点击的位置
    private ViewGroup viewRoot;//父容器
    private int location = TOP;//绘制位置
    private int choose = -1;//点击的item
    private int mToolbarBottom;//Toolbar下边距屏幕上距离
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;//windowManger布局管理器,为了像Dialog一样在Activity弹出,而不是依附于某个group
    private onItemCilckLinener itemCilckLinener;
    private Context context = null;

3 构造函数以及初始化方法

private MyTipView(Context context, int x, int y, ViewGroup viewRoot, List<TextItem> items) {
        super(context);
        this.viewRoot = viewRoot;
        this.context = context;
        this.x = x;
        this.y = y;
        this.items = items;
        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        layoutParams = new WindowManager.LayoutParams();
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;//窗口的宽
        layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;//窗口的高
        //设置LayoutParams的属性
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;//该Type描述的是形成的窗口的层级关系,下面会详细列出它的属性
        layoutParams.format = PixelFormat.TRANSLUCENT;//不设置这个弹出框的透明遮罩显示为黑色
        //layoutParams.token = viewRoot.getWindowToken();//设置Token
        int[] location = new int[2];
        viewRoot.getLocationInWindow(location);//获取在当前窗口内的绝对坐标
        viewRoot.getLocationOnScreen(location);//获取在整个屏幕内的绝对坐标
        mToolbarBottom = location[1];//[0]是x轴坐标,[1]y轴
        windowManager.addView(this, layoutParams);
        init();
        initView();
    }

    //初始化画笔
    private void init() {
        mPaint = new Paint();
        mSPaint = new Paint();
        mPath = new Path();
        mSeparationPaint = new Paint();
        mSeparationPaint.setStyle(Paint.Style.FILL);

        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(Sp2Px(14));
        mPaint.setColor(Color.BLACK);


        mSPaint.setAntiAlias(true);
        mSPaint.setStyle(Paint.Style.FILL);
        mSPaint.setColor(Color.BLACK);

        //初始变量
        mItemWidth = Dp2Px(50);
        mItemHeight = Dp2Px(48);
        mTriaHeight = Dp2Px(10);//三角的高度
        mHalfTriaWidth = Dp2Px(6);//三角的半宽
        mTriaAcme = Dp2Px(6);//三角的顶点
        marginSide = Dp2Px(4);//左右边距
        mItemSeparation = Dp2Px(1);//分割线宽度;
        mRadius = Dp2Px(6);//圆角
        mTextColor = Color.WHITE;
        mTextSize = Sp2Px(14);
    }

4 计算三角顶点位置

private void initView() {
        int count = items.size();
        int width = count * mItemWidth + mItemSeparation * (count - 1);
        int mScreenWidth = getResources().getDisplayMetrics().widthPixels;
        if (y - mToolbarBottom < (mItemHeight + mTriaHeight + mTriaAcme)) {
            location = DOWN;//下方显示
            mTriaAcme += y;//设置三角顶点y轴值;
            mTriaItemBorder = mTriaAcme + mTriaHeight;//计算三角方块交界y
        } else {
            location = TOP;
            mTriaAcme = y - mTriaAcme;//计算顶点位置y轴值
            mTriaItemBorder = mTriaAcme - mTriaHeight;//计算三角方块交界y值
        }
        if (x < (width / 2 + marginSide)) {
            realLeft = marginSide;//计算最左侧距离屏幕左边距离,左边撑不下
        } else if ((mScreenWidth - x) < (width / 2 + marginSide)) {
            realLeft = mScreenWidth - marginSide - width;//计算最左侧距离屏幕左边距离,右边撑不下
        } else {
            realLeft = x - width / 2;//计算最左侧距离屏幕左边距离,触碰不到边界
        }

    }

5 设置背景为透明

private void drawBackground(Canvas canvas) {
        canvas.drawColor(Color.TRANSPARENT);
    }

6 绘制三角

private void drawTop(Canvas canvas) {
        //绘制三角
        mPath.reset();
        mPath.moveTo(x, mTriaAcme);
        mPath.lineTo(x - mHalfTriaWidth, mTriaAcme - mTriaHeight);
        mPath.lineTo(x + mHalfTriaWidth, mTriaAcme - mTriaHeight);
        canvas.drawPath(mPath, mSPaint);
        MyDraw(canvas, mTriaItemBorder - mItemHeight);
    }

    private void drawDown(Canvas canvas) {

        //绘制三角
        mPath.reset();//清理路径
        mPath.moveTo(x, mTriaAcme);
        mPath.lineTo(x - mHalfTriaWidth, mTriaAcme + mTriaHeight);
        mPath.lineTo(x + mHalfTriaWidth, mTriaAcme + mTriaHeight);
        canvas.drawPath(mPath, mSPaint);
        //绘制方块
        MyDraw(canvas, mTriaItemBorder);
    }

7 绘制方块

绘制时因为第一个和最后一个方块带有圆角,单独绘制

private void MyDraw(Canvas canvas, int t) {
        //绘制item
        int count = items.size();
        int width = (count - 1) * mItemSeparation + count * mItemWidth;
        int l = realLeft + mItemWidth + mItemSeparation;
        mItemRectList.clear();
        for (int i = 0; i < items.size(); i++) {
            if (choose == i) {//当前是否被点击,改变颜色
                mPaint.setColor(Color.DKGRAY);
            } else {
                mPaint.setColor(Color.BLACK);
            }
            if (i == 0) {//绘制第一个带圆角的item
                mPath.reset();
                mPath.moveTo(realLeft + mItemWidth, t);
                mPath.lineTo(realLeft + mRadius, t);
                mPath.quadTo(realLeft, t, realLeft, t + mRadius);
                mPath.lineTo(realLeft, t + mItemHeight - mRadius);
                mPath.quadTo(realLeft, t + mItemHeight, realLeft + mRadius, mItemHeight + t);
                mPath.lineTo(realLeft + mItemWidth, t + mItemHeight);
                canvas.drawPath(mPath, mPaint);
                mSeparationPaint.setColor(mSeparateLineColor);
                canvas.drawLine(realLeft + mItemWidth, t, realLeft + mItemWidth,
                        t + mItemHeight, mSeparationPaint);
            } else if (i == (items.size() - 1)) {//绘制最后一个
                mPath.reset();
                mPath.rMoveTo(realLeft + width - mItemWidth, t);
                mPath.lineTo(realLeft + width - mRadius, t);
                mPath.quadTo(realLeft + width, t, realLeft + width, t + mRadius);
                mPath.lineTo(realLeft + width, t + mItemHeight - mRadius);
                mPath.quadTo(realLeft + width, t + mItemHeight, realLeft + width - mRadius, t + mItemHeight);
                mPath.lineTo(realLeft + width - mItemWidth, t + mItemHeight);
                canvas.drawPath(mPath, mPaint);
            } else {//绘制中间方块和分割线
                mPath.reset();
                mPath.moveTo(l, t);
                mPath.lineTo(l + mItemWidth, t);
                mPath.lineTo(l + mItemWidth, t + mItemHeight);
                mPath.lineTo(l, t + mItemHeight);
                canvas.drawPath(mPath, mPaint);
                canvas.drawLine(l + mItemWidth, t, l + mItemWidth, t + mItemHeight,
                        mSeparationPaint);
                l += mItemWidth + mItemSeparation;
            }
            mItemRectList.add(new Rect(realLeft + i * (mItemSeparation + mItemWidth), t, realLeft + i * (mItemSeparation + mItemWidth) + mItemWidth, t + mItemHeight));
        }
    }

最后一行代码

mItemRectList.add(new Rect(realLeft + i * (mItemSeparation + mItemWidth), t, realLeft + i * (mItemSeparation + mItemWidth) + mItemWidth, t + mItemHeight));

用一个List来存放Rect(矩形),这些矩形对应的是每一个item的方块,但是并没有绘制出来,只是存放起来,矩形是为了在绘制文字的时候提供文字居中时用到的。

8 绘制文字

private void drawTitle(Canvas canvas) {
        for (int i = 0; i < items.size(); i++) {
            Rect rect = mItemRectList.get(i);//用于文字居中
            //mPaint.setColor(Color.WHITE);
            Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
            p.setAntiAlias(true);
            p.setStrokeWidth(3);
            int s = Dp2Px(items.get(i).getTextSize());
            p.setTextSize(mTextSize);
            if (s != 0)//如果在TextItem中设置了size,就是用设置的size
                p.setTextSize(s);
            p.setColor(mTextColor);
            Paint.FontMetricsInt fontMetricsInt = p.getFontMetricsInt();
            p.setTextAlign(Paint.Align.CENTER);
            float baseline = (rect.bottom + rect.top + fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;//文字居中,基线算法
            canvas.drawText(items.get(i).getTitle(), rect.centerX(), baseline, p);
        }
    }

9 点击变色,以及点击事件实现

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                for (int i = 0; i < items.size(); i++) {
                    if (itemCilckLinener != null && isPointInRect(new PointF(event.getX(), event.getY()), mItemRectList.get(i))) {
                        choose = i;//记录点击item编号
                        Rect rect = mItemRectList.get(i);
                        postInvalidate(rect.left, rect.top, rect.right, rect.bottom);//刷新视图
                        return true;
                    }
                }
                removeView();//点击item以外移除
                return false;
            case MotionEvent.ACTION_UP:
                for (int i = 0; i < items.size(); i++) {
                    if (itemCilckLinener != null && isPointInRect(new PointF(event.getX(), event.getY()), mItemRectList.get(i))) {
                        if (i == choose) {//与down的item一样时才触发
                            itemCilckLinener.onItemCilck(items.get(i).getTitle(), i);//触发点击事件
                            removeView();
                            return true;
                        }
                    } else {//点下后移动出item,初始化视图
                        postInvalidate();//刷新视图
                    }
                }
                choose = -1;//重置
                return false;
        }
        return false;
    }
 /**
     * 判断这个点有没有在矩形内
     *
     * @param pointF
     * @param targetRect
     * @return
     */
    private boolean isPointInRect(PointF pointF, Rect targetRect) {
        if (pointF.x < targetRect.left) {
            return false;
        }
        if (pointF.x > targetRect.right) {
            return false;
        }
        if (pointF.y < targetRect.top) {
            return false;
        }
        if (pointF.y > targetRect.bottom) {
            return false;
        }
        return true;
    }

10 Builder模式创建

 public static class Builder {
        private List<TextItem> items = new ArrayList<>();
        private int x = 0, y = 0;
        private Context context;
        private ViewGroup viewRoot;
        private onItemCilckLinener itemCilckLinener;
        private int mRadius;

        public Builder(Context context, ViewGroup viewRoot) {
            this.context = context;
            this.viewRoot = viewRoot;
        }
        public Builder addItem(TextItem item) {
            items.add(item);
            return this;
        }
        public Builder setmRadius(int radius) {
            mRadius = radius;
            return this;
        }
        public Builder setxAndy(int x, int y) {
            this.x = x;
            this.y = y;
            return this;
        }
        public Builder setOnItemClickLinener(onItemCilckLinener itemClickLinener) {
            this.itemCilckLinener = itemClickLinener;
            return this;
        }
        public MyTipView create() {
            if (items.size() == 0) {
                try {
                    throw new Exception("item count is 0");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            MyTipView myTipView = new MyTipView(context, x, y, viewRoot, items);
            myTipView.setItemCilckLinener(itemCilckLinener);
            if (mRadius != 0)
                myTipView.setRadius(mRadius);
            return myTipView;
        }
    }

11 item

//TipView的item
    public static class TextItem {
        private String title;
        private int textSize;
        private int textColor = Color.WHITE;

        public TextItem(String title) {
            this.title = title;
        }
        public TextItem(String title, int textSize) {
            this.title = title;
            this.textSize = textSize;
        }
        public TextItem(String title, int textSize, int textColor) {
            this.title = title;
            this.textSize = textSize;
            this.textColor = textColor;
        }
        public String getTitle() {
            return title;
        }
        public void setTitle(String title) {
            this.title = title;
        }
        public int getTextSize() {
            return textSize;
        }
        public void setTextSize(int textSize) {
            this.textSize = textSize;
        }
        public int getTextColor() {
            return textColor;
        }
        public void setTextColor(int textColor) {
            this.textColor = textColor;
        }
    }

12 使用示例

MyTipView.Builder builder = new MyTipView.Builder(this, linearLayout);
        builder.addItem(new MyTipView.TextItem("1"))
                .addItem(new MyTipView.TextItem("2"))
                .addItem(new MyTipView.TextItem("3"))
                .addItem(new MyTipView.TextItem("4"))
                .setxAndy((int) x, (int) y)
                .setOnItemClickLinener(new MyTipView.onItemCilckLinener() {
                    @Override
                    public void onItemCilck(String title, int i) {
                        Toast.makeText(MainActivity.this, title, Toast.LENGTH_SHORT).show();
                    }
                })
                .create();

13 源码

https://github.com/liujiakuoyx/learn/tree/master/TipView/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,667评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,361评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,700评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,027评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,988评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,230评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,705评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,366评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,496评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,405评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,453评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,725评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,803评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,015评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,514评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,111评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,287评论 25 707
  • 1 CALayer IOS SDK详解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi阅读 5,114评论 3 23
  • 下班的时候,已经是灯火阑珊,夜色撩人,秋意渐浓。 一个人走回来的路上,看到周边的男男女女老老少少,突然...
    柠C小姐阅读 431评论 0 0
  • 文:ICE蝈蝈 很多人看着别人风光无限,游山玩水,内心不免涟漪不断。更有甚者不理解自己怎么就能过得苦哈哈呢? 人前...
    ICE蝈蝈阅读 250评论 2 2
  • 同事家有个6岁的小姑娘,人长的高高瘦瘦,却无饭不欢。幼儿园4点放学回家就要吃晚饭,如果让她吃点饼干垫垫肚子,她会说...
    35梦想阅读 261评论 0 0