1

序言(扯)

由于现在大公司放出的实习offer大多为暑期,本学期课程不多,所以找了一家公司可以立刻入职的去实习下,相比大公司,小公司多采取硬上的策略,对于解决编程问题能力的提升是相当大的,在拉勾投了一家,前天去面试,然后一个自定义view把我问跪了,讲到复用性,讲了很不优雅的一种实现方式,不过最终学习了下,实现思路和我当时讲的差不多,但这确实是我开发中一个短板,但是其他问题回答的还是蛮不错的,还是给了offer,产品真心赞,团队也超赞,CEO=CMU(CS),听COO讲了下产品,分析的真是让我瞠目结舌。下周入职,搬,搬,搬。

今天来写自定义View了,之前看任主席的书,看了View的绘制和事件响应机制,对view底层有了较深理解,但是说到自定义一个View,下不去手。写个卫星菜单,后续将根据这个项目中出现的问题,然后在后续对自定义view的问题和view底层的一些东西进行讲解下。

自定义控件过程

自定义控件的流程大致分为以下几步

  • 定义属性

  • 定义xml文件

  • 自定义View获取属性

  • onMeasure()

  • onLayout()

自定义view的目的是为了提升我们的视觉体验,所以一般我们会辅助一些动画来提升体验,为了增强交互,我们也要为其增加一些交互,所以我么需要对其中进行一些事件的监听,自定义监听器,然后在我们需要的地方进行回调,考虑完这些,我们需要再考虑的是如何提高这些view的复用性。还有当我们多个空

View的绘制流程

  • OnMeasure

  • OnLayout

  • OnDraw

对于自定义View,我们通常会重写这三个方法,重写那些,取决于我们的自定义View从哪里继承,然后要实现什么样的功能。大致归纳有以下几点。

  • 继承View
    实现一些不规则的图形,需要重写onDraw方法进行绘制

  • 继承ViewGroup
    需要实现对于子控件的测量和布局

  • 继承特定View
    较容易实现

  • 继承特定ViewGroup
    无需处理测量和布局

实现卫星菜单

有了前面的一点小储备,接下里要动手实现我们的小demo了,当然我这个demo的实现思路也是参考网上的思路和实现,首先明确我们要实现的样式是如何的。上一张图
图片描述
如上图所示,我们需要一个按钮,触发后向发生卫星弹射出5个子View,然后点击之后会缩回,根据上面的自定义View的分类,我们不难发现,我们需要的是通过继承ViewGroup来实现,继承自ViewGroup,那么我们要对其进行一个测量,然后是布局,难点就是在布局上,如何布局呢?这里不难发现,我们可以通过为其设定半径,然后将这个几个卫星平均分布在中心的圆周围。然后对于我们中间的View进行事件的监听,还有对于分布在周围的View的监听。上述即为实现的核心环节。接下来按照我们的思路,贴出代码,供以参考,熟悉整个流程。

  • 属性设置
    这个之前真的没有用到过,又涨姿势了)喜悦脸,之前在使用一些自定义控件中,我们不难发现会有一些特殊的属性,我们可以为其设置值,然后我们使用的自定义View就可以根据这个属性制定的值进行显示,如何设置这些值,然后在View的内部又是如何获得的这些值呢?

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="postion">
        <enum name="left_top" value="0"/>
        <enum name="left_bottom" value="1"/>
        <enum name="right_top" value="2"/>
        <enum name="right_bottom" value="3"/>
    </attr>
    <attr name="radius" format="dimension"/>

    <declare-styleable name="ArcMenu">
        <attr name="postion"/>
        <attr name="radius"/>
    </declare-styleable>

</resources>

在values下,设置我们的资源属性文件,然后声明我们的自定义View所需要的属性,然后在我们自定义View添加命名空间之后,我们就可以使用这些属性了,这都是很次要的了,然后完成我们的布局。

  • 设置布局

<com.example.chenjensen.viewset.arcmenuview.ArcMenu
        android:id="@+id/arc_menu"
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:postion="left_bottom"
        app:radius="100dp"
       >

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@mipmap/arcmenu_composer_button">
            <ImageView
                android:id="@+id/arcmenu_iv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:src="@mipmap/arcmenu_composer_icn_plus"/>
        </RelativeLayout>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/arcmenu_composer_camera"
            android:tag="camera"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/arcmenu_composer_music"
            android:tag="music"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/arcmenu_composer_place"
            android:tag="place"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/arcmenu_composer_sleep"
            android:tag="sleep"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/arcmenu_composer_thought"
            android:tag="thought"/>

    </com.example.chenjensen.viewset.arcmenuview.ArcMenu>

在View中获取这些属性

 TypedArray a = context.getTheme().obtainStyledAttributes(attributeSet, R.styleable.ArcMenu,defStyle,0);
        int pos = a.getInt(R.styleable.ArcMenu_postion, POS_RIGHT_BOTTOM);
        switch (pos)
        {
            case POS_LEFT_BOTTOM:
                mPostion = Position.LEFT_BOTTOM;
                break;
            case POS_LEFT_TOP:
                mPostion = Position.LEFT_TOP;
                break;
            case POS_RIGHT_BOTTOM:
                mPostion = Position.RIGHT_BOTTOM;
                break;
            case POS_RIGHT_TOP:
                mPostion = Position.RIGHT_TOP;
                break;
            default:
                break;

        }
        mRadius = (int)a.getDimension(R.styleable.ArcMenu_radius,(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100
                ,getResources().getDisplayMetrics()));
  • 测量子View
    因为我们继承的是一个ViewGroup, 所以我们需要进行子View的测量。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        for(int i=0; i<count; i++){
            measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
  • View布局

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(changed){
            layoutCButton();
            int count = getChildCount();
            for(int i=0; i<count-1; i++){
                View child = getChildAt(i+1);
                child.setVisibility(GONE);
                int cl = (int)(mRadius*Math.sin(Math.PI/2/(count-2)*i));
                int ct = (int)(mRadius*Math.cos(Math.PI/2/(count-2)*i));
                int cWidth = child.getMeasuredWidth();
                int cHeight = child.getMeasuredHeight();
                if(mPostion==Position.LEFT_BOTTOM||mPostion==Position.RIGHT_BOTTOM){
                    ct = getMeasuredHeight()-cHeight-ct;
                }
                if(mPostion==Position.RIGHT_BOTTOM||mPostion==Position.RIGHT_TOP){
                    cl = getMeasuredWidth()-cWidth-cl;
                }
                child.layout(cl,ct,cl+cWidth,ct+cHeight);
            }
        }
    }

这里涉及到我们在布局上的数学知识了。因为当我们的卫星菜单处在四个不同的角的时候,我们在坐标的设置上就会出现问题了,首先是将我们的恒星按钮进行布局,然后是卫星按钮围绕其周围进行布局,对于恒星的布局,也需要我们根据四个不同的位置进行选择。

private void layoutCButton(){
        mCButton = getChildAt(0);
        mCButton.setOnClickListener(this);
        int left = 0;
        int top = 0;
        int width = mCButton.getMeasuredWidth();
        int height = mCButton.getMeasuredHeight();
        switch (mPostion){
            case LEFT_BOTTOM:
                left = 0;
                top = getMeasuredHeight()-height;
                break;
            case RIGHT_BOTTOM:
                left = getMeasuredWidth()-width;
                top = getMeasuredHeight()-height;
                break;
            case LEFT_TOP:
                left = 0;
                top = 0;
                break;
            case RIGHT_TOP:
                left = getMeasuredWidth()-width;
                top = 0;
                break;
            default:break;
        }
        mCButton.layout(left, top, left + width, top + height);
    }

这样我们的基本完成了界面的布局了,然后是对于

  • 点击事件的监听和响应

@Override
    public void onClick(View v) {
        rotateButton(v, 0f, 360f, 300);
        toggleMenu(300);
    }

这里为了增强体验感,对于按钮,在点击的时候添加了一个旋转动画,然后是控制卫星的弹射,可能会有点疑问哈,我们之前写代码对于按钮事件的监听,我们不都是要将其和具体的控件绑定吗,但是这里为什么不需要,原因是,在这里只有它可以响应这个事件,其它的都是不可见,在可见的时候,我们又为其设置了监听器,一会演示,因此事件会被子view自己的监听事件拦截掉,而不会在其父view级的事件处理机制中出现。这里涉及到View的事件传递机制,是从父View向子View进行传递,如果事件被消耗了,则父View没有响应,当然可以在父View中设置拦截机制。这里的toggleMenu的实现是响应我们的卫星向周边弹射的核心。

public void toggleMenu(int duration){
        int count = getChildCount();
        for(int i=0; i<count-1; i++){
            final View view = getChildAt(i+1);
            view.setVisibility(VISIBLE);
            int cl = (int)(mRadius*Math.sin(Math.PI/2/(count-2)*i));
            int ct = (int)(mRadius*Math.cos(Math.PI / 2 / (count - 2) * i));
            int xFlag = 1;
            int yFlag = 1;
            if(mPostion==Position.LEFT_BOTTOM||mPostion==Position.LEFT_TOP)
                xFlag = -1;
            if(mPostion==Position.LEFT_TOP||mPostion==Position.RIGHT_TOP)
                yFlag = -1;
            AnimationSet set = new AnimationSet(true);
            Animation transAnim = null;
            //添加平移动画
            if(mCurrentStatus == Status.CLOSE){
                transAnim = new TranslateAnimation(xFlag*cl,0,yFlag*ct,0);
                view.setClickable(true);
                view.setFocusable(true);
            }else{
                transAnim = new TranslateAnimation(0,xFlag*cl,0,yFlag*ct);
                view.setClickable(false);
                view.setFocusable(false);
            }
            transAnim.setFillAfter(true);
            transAnim.setDuration(duration);
            transAnim.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                }
                @Override
                public void onAnimationEnd(Animation animation) {
                    if(mCurrentStatus==Status.CLOSE){
                        view.clearAnimation();
                        view.setVisibility(View.INVISIBLE);
                    }
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            //添加旋转动画
            RotateAnimation animation = new RotateAnimation(0,720, Animation.RELATIVE_TO_SELF,0.5f
                    ,Animation.RELATIVE_TO_SELF,0.5f);
            animation.setDuration(duration);
            animation.setFillAfter(true);
            //先添加旋转,后添加平移动画
            set.addAnimation(animation);
            set.addAnimation(transAnim);
            view.startAnimation(transAnim);
            final int pos = i;
            view.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(mMenuItemClickListener!=null){
                        mMenuItemClickListener.onClick(v,pos);
                    }
                    menuItemAnim(pos);
                    changeState();
                }
            });
        }
        changeState();
    }

说下实现原理吧,因为这里我们使用的不是属性动画,通过的是平移,其特点是当其移动了之后,其实际位置还是在原处,所以是不可以点击的,解决这个问题的方式是将其固定在我们要点击的位置,然后设置为不可见,移动的时候,让其从一个负位置移动到当前这个位置,产生一种发射的特效,同时给其添加了一个旋转特效,让其显得更加的自然圆滑,之后,我们对这些子View设置监听事件,监听事件是让点击的按钮变大,未点击的按钮消失,然后变化这些状态。

private void menuItemAnim(int pos){
        for(int i=0; i<getChildCount()-1; i++){
            View childView = getChildAt(i+1);
            if(i==pos){
                childView.startAnimation(scaleBigAnimation(300));
            }else{
                childView.startAnimation(scaleSmallAnimation(300));
            }
            childView.setFocusable(false);
            childView.setClickable(false);
        }
    }

遇到的问题

  • 在对View进行设置可见或者不可见的时候,要清除掉当前的动画

结束语

具体动画代码不再贴出,可以去我的Github上查看具体代码,同时本人想实现一个自定义View的集合篇,将写一些自定义View的东西具体的实现贴一下,然后在Github上挂出代码, 大家有什么推荐的吗?这样我们可以一起学习,共同进步。


Jensen95
2.9k 声望534 粉丝

连续创业者。