android自定义View——实现Dribbble的[Open & Close]设计

本文介绍如何实现Dribbble上热门的汉堡按钮动画效果,包括动画原理分析、核心代码解析及在Android上的实现技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、提要

Open & Close 在Dribbble的Popular程度能排在所有Shots的首页。而且设计比较简洁,实现起来的难度也相对较小,可以拿来练练手。本文源码猛击:Roujiamo

2、分析

动画开始前是经典的hamburger,由上中下三条直线组成,以l1、l2、l3表示,动画结束后变成了关闭按钮。

关闭的“X“是由hamburger的l1、l3经过旋转变换而来。其中l1绕右端点逆时针旋转45°,l3绕右端点顺时针旋转45°。旋转后,l1左右端点的y坐标分别与l3左右端点的坐标相同,并且交点位于整个画面的中心。这说明不仅仅有旋转变换,还发生了x轴负方向的位移变换,否则交点肯定是偏右的。为了保证旋转后各点y坐标相同,对单条直线长度lineLength和l1、l3之间的距离height做一定限制。根据三角函数的相关知识可以很快算出,height = lineLength  * sin(45°)。同样根据三角函数得知,直线旋转后,其在x轴的映射为lineLength * cos(45°),也就是说向x轴的负方向平移了lineLength * (1- cos(45°))  / 2的距离。

然后再分析l2和外接圆的变化,l2向右平移并逐渐变换为接近外接圆弧的曲线,当l2开始变成圆弧时,圆弧的弧度开始逐渐变大同时逆时针旋转。这个直线逐渐变换为曲线的动画我还没想到具体要怎么去实现,可以参考一下这篇博客:Making a SVG HTML Burger Button。他是用svg来实现的,粗略判断其中曲线变换部分其实只是移动旋转一张图片,这张图片就是一条过度曲线(如有错误,欢迎指出)。本文不打算实现曲线变换这一部分,会以另一种方式来替代这段动画。首先让l2往x轴正方向平移,直到l2左端点到达外接圆,同时保持右端点不超过外接圆。当右端点到达外接圆时,外接圆变换开始,圆弧起点0°到360°,整个过程逆时针旋转135°。

最后,从gif图中可以看出,很多动画都不是线性变换的,那就要用到android中的插值器了。详细可参考Android中的Interpolator,对比文中给出的数学曲线可以很快找到我们需要的插值器。其中l1、l3可以用AnticipateOvershootInterpolator,l2使用AnticipateInterpolator,圆弧则用AccelerateDecelerateInterpolator。使用插值器就出现了下面这个问题,l2右端点具体什么时候到达圆弧,这个时间点比较不好算,需要解一元三次方程。本文给这个时间点设置为定值,虽然并不准确,但是误差很小,可以接受。

3、设计实现

根据上面的分析,对于要实现什么已经有了比较清晰的轮廓,但是除了“画”这个功能,我们还需要保存状态,当屏幕切换时,能够恢复到之前的状态。此外还要监听点击事件,触发动画。可以把“画”这个功能单独抽离出来,用Drawable来实现,其余的放在view里实现。这样做的好处是,最主要的功能可以不依赖于任何View,实际应用起来限制更少。在最后应用一节会认识到这样做的优势。

先从简单的部分开始入手,既然用了Drawable,View则选用ImageView,因为这个类可以通过setImageDrawable方法来设置Drawable,比较方便。监听状态很简单,略过。直接看状态保存,其中有两种状态:open和close,只要一个boolean的变量来保存。说到保存状态,自然而然的就可以想到Activity的onSaveInstanceState和onRestoreInstanceState。这两个方法在View里面也有。这样我们只需要分别在这两个方法中保存和恢复状态即可。可以定义一个类来充当数据model的角色,当然你也可以不这么做,直接往Bundle里writeInt。下面来看代码吧:

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        SavedState ss = new SavedState(superState);

        //获取当前的状态
        ss.open = drawable.isOpen();

        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if(!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        final SavedState ss = (SavedState)state;
        super.onRestoreInstanceState(ss.getSuperState());

        post(new Runnable() {

            @Override
            public void run() {
                //设置当前状态并重绘,三个参数分别表示:当前状态,是否需要动画,是否重绘
                setOpen(ss.open, false, true);
            }
        });
    }

    private static class SavedState extends BaseSavedState {
        boolean open;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            this.open = in.readInt() == 1;
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(this.open ? 1 : 0);
        }

        public static final Parcelable.Creator<SavedState> CREATOR =
                new Parcelable.Creator<SavedState>() {
                    public SavedState createFromParcel(Parcel in) {
                        return new SavedState(in);
                    }

                    public SavedState[] newArray(int size) {
                        return new SavedState[size];
                    }
                };
    }
代码都很简单,没有太多注释,其中两个方法已经给出了说明,具体实现暂时不用管,知道它的功能就行了。需要提醒的是,android HONEYCOMB及之后的版本都有硬件加速的功能,最好在View里面禁用,否则在动画播放的时候旋转屏幕的话会出现bug。可以通过以下代码来禁用硬件减速:
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

最后来看重头戏吧,本文只介绍主体的流程,细节上的分析读者可自行阅读源码。上面已经分析过了,无非就是画三条直线一条曲线嘛。由于l1、l3都是绕自己右端点来旋转的,除去为了保证居中而做的x轴负方向的位移外,我们可以只看做只有左端点在绕着右端点来旋转。这样就把线段的旋转转换成了点的旋转。那好,假设旋转之后的点的坐标以及x轴负方向的位移我们都知道了,那么要画出图形来就很简单了。

    @Override
    public void draw(Canvas canvas) {
        // translate and rotate  topStartRotated l1左端点旋转后的坐标,  topEnd l1右端点,  translateX x轴负方向的位移
        canvas.drawLine(topStartRotated.x - translateX, topStartRotated.y, topEnd.x - translateX, topEnd.y, paint);
        // just translate  middleTranslateStart l2位移后的左端点, middleTranslateEnd l2位移后的右端点
        canvas.drawLine(middleTranslateStart.x, middleTranslateStart.y, middleTranslateEnd.x, middleTranslateEnd.y, paint);
        // arc 外接圆的轮廓, arcStartAngle 圆弧的起始角度, arcSweepAngle 圆弧的角度
        canvas.drawArc(arc, arcStartAngle, arcSweepAngle, false, paint);
        // translate and rotate  bottomStartRotated l3左端点旋转后的坐标,  bottomEnd l3右端点
        canvas.drawLine(bottomStartRotated.x - translateX, bottomStartRotated.y, bottomEnd.x - translateX, bottomEnd.y, paint);
    }
刚才提到点的旋转,那么我们怎么计算某个点绕另一个点旋转后的坐标呢?这里给一个公式:
        // rotate
        // (x0,y0) is after (x,y) rotating around (rx0, ry0)
        // x0= (x - rx0)*cos(a) - (y - ry0)*sin(a) + rx0 ;
        // y0= (x - rx0)*sin(a) + (y - ry0)*cos(a) + ry0 ;
(x0,y0)即是点(x,y)绕点(rx0,ry0)旋转a度后的坐标。起始点(x,y)的坐标可以放到onBoundsChange方法中计算,只需保证整体居中,且l1、l3之间的高度height = lineLength  * sin(45°)。

关键点也介绍了,下面要说动画的流程了。说白了,动画就是通过不断的重绘来实现的。本文另起一个线程来做这部分工作。不断计算动画进行了多长时间来更新动画的进度,同时更新4条线的位置,最后通知View重绘。为了节省资源,大约20ms重绘一次。动画开始时,记录下当前时间作为动画的开始时间,每次循环都取系统当前时间减去动画开始时间,这就是动画的进度。当动画close时,其实就是open的倒带。只需简单的将动画开始时间减去当前时间并加上动画的时长。通知View重绘可以通过invalidateSelf方法,但是直接在非UI线程了调用这个方法是不行的,用scheduleSelf可以解决这个问题。

    private Runnable mInvalidateTask = new Runnable() {
        @Override
        public void run() {
            invalidateSelf();
        }
    };

    private void toggleAnim(){
        //动画进行进度比例,open为动画结束后,状态是否为open
        float percent = open ? 0 : 1;
        //动画进度
        int timeLapse;
        //当前时间
        long cur;
        float tmp;
        //动画开始时间
        long animStartTime = SystemClock.uptimeMillis();
        while(percent <= 1 && percent >= 0) {
            cur = SystemClock.uptimeMillis();
            if (open) {
                timeLapse = (int) (cur - animStartTime);
            } else {
                timeLapse = (int) (BurgerDrawable.DURATION + animStartTime - cur);
            }
            percent = (float) timeLapse / BurgerDrawable.DURATION;
            tmp = Math.min(1, percent);
            tmp = Math.max(0, tmp);
            // 更新四条曲线的动画进度,即更新其位置,详见源码
            setPercentage(tmp, false);
            scheduleSelf(mInvalidateTask, cur);
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        animating = false;
    }
注意到我们取系统时间用的是SystemClock.uptimeMillis()方法,而不是System.currentTimeMIllis()。前者取的是开机到现在为止的时间,而后者取的是系统设置的当前时间,后者有可能会被修改,而前者是不能被修改的。

4、应用

Burger按钮当然是要结合ActionBar来一起用啦,点击Burger时,左侧菜单弹出或收起。本文的左侧菜单用的是Android自带的DrawerLayout。新建一个Navigation Drawer Activity,Android Studio的步骤是右击源码目录--> new--> Activity--> Navigation Drawer Activity。该操作会自动生成Activity、Fragment等文件。直接打开NavigationDrawerFragment.java,注意到其中有一个类ActionBarDrawerToggle,它是用来控制HomeAsUp图标动画的,所以我们要做的工作跟它是一样的。这个类在support-v4包中,直接拷过来,发现缺少两个引用的类,ActionBarDrawerToggleHoneycomb和ActionBarDrawerToggleJellybeanMR2,一并拷出来。这两个类其实是为了兼容不同版本的setHomeAsUpIndicator方法,来将HomeAsUp图标设置为我们的Drawable。最后,把它的mSlider改成我们实现的Drawable,调用到的mSlider的方法也修改成我们的,比如设置状态。整个ActionBarDrawerToggle类代码较多,但是我们改动的地方很少,这里就不贴了,详见源码。

最后,我们实现的效果如下:


Android自定义View 星球运动在dribbble闲逛的时候发现的一个有意思的星球运动的动画,刚好最近时间尚可,就简单实现了一下中间运动的部分,又是因为时间的原因,开头位移的部分没有完成.&nbsp;这是在dribbble中发现的动画&nbsp;这是我自己实现的效果... 总觉得我这个星球有点胖... 因为胖所以转的慢么这是.速度等细节还有优化的余地设计过程老办法,先分解动画的构成.整个动画可以看做是一个自旋的星球从右上角由小变大的移动到屏幕的中央的.星球的位移及缩放不说(其实是最近有需求,暂时没时间完善),主要完善了星球的旋转及尾部的处理.最底层是背景的星星闪烁,每次在星球一定范围内随机出现,并缩放就好最开始设计尾部效果的时候,是在没列中设计了两端线.再不断的运行及移动.但是实现起来很乱.最后采用了先绘制所有尾部展示的内容,然后在用和背景一样的颜色部分遮盖并移动此部分形成视觉上的效果的方法.(也可以设置PorterDuff模式来展示).设计过程中的效果如下星球的设计,星球的本身使用简单的遮盖和贝塞尔曲线就能完成一个较为满意的星球背景.重点是星球地表的设计以及星球自转下的地表样式的移动.解决的方法是是先绘制三个重复并连续的地表样式,通过移动整个地表样式模拟星球的转动.最后通过PorterDuff来控制展示的部分和星球的位置重合.未开启PorterDuff模式时绘制的样式如下:开启PorterDuff模式后再指定位置展示指定形状的图形如下:最后再移动设置好的星球地貌就可以模拟出星球转动的效果了代码实现背景的星星private&nbsp;fun&nbsp;drawStarts(canvas:&nbsp;Canvas,&nbsp;perIndexInAll:&nbsp;Float)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;//背景的星星在星球附近的一定范围内随机出现 &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;maxRand&nbsp;=&nbsp;800 &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(-maxRand&nbsp;/&nbsp;2F&nbsp;,&nbsp;-maxRand&nbsp;/&nbsp;2F) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;Random&nbsp;=&nbsp;Random(perIndexInAll.toInt().toLong()) &nbsp;&nbsp;&nbsp;&nbsp;//绘制背景的星星 &nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;(index&nbsp;in&nbsp;0..4){ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;drawStart(canvas&nbsp;,&nbsp;&nbsp;Random.nextFloat()&nbsp;*&nbsp;maxRand&nbsp;,&nbsp;Random.nextFloat()&nbsp;*&nbsp;maxRand&nbsp;,&nbsp;perIndex) &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(maxRand&nbsp;/&nbsp;2F&nbsp;,&nbsp;maxRand&nbsp;/&nbsp;2F) } //绘制背景的星星内容 //绘制背景的星星内容 private&nbsp;fun&nbsp;drawStart(canvas:&nbsp;Canvas,&nbsp;x:&nbsp;Float,&nbsp;y:&nbsp;Float,&nbsp;per:&nbsp;Float)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;per&nbsp;=&nbsp;per &nbsp;&nbsp;&nbsp;&nbsp;//这个部分是为了让星星实现从小到大后再从大到小的变动 &nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(per&nbsp;&gt;=&nbsp;1.0F){ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;per&nbsp;-=&nbsp;1F &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(per&nbsp;&lt;=&nbsp;0.5F){ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;per&nbsp;*=&nbsp;2 &nbsp;&nbsp;&nbsp;&nbsp;}else{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;per&nbsp;=&nbsp;(1&nbsp;-&nbsp;per)&nbsp;*&nbsp;2 &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(x&nbsp;,&nbsp;y) &nbsp;&nbsp;&nbsp;&nbsp;canvas.scale(per&nbsp;,&nbsp;per) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paint&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;0xff78D8DF.toInt() &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;startLength&nbsp;=&nbsp;30F &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;startOffset&nbsp;=&nbsp;startLength&nbsp;/&nbsp;3F &nbsp;&nbsp;&nbsp;&nbsp;//通过路径描绘星星的形状 &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;path&nbsp;=&nbsp;Path() &nbsp;&nbsp;&nbsp;&nbsp;path.moveTo(0F&nbsp;,&nbsp;startLength) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(startOffset&nbsp;,&nbsp;startOffset&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(startLength&nbsp;,&nbsp;0F) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(startOffset&nbsp;&nbsp;,&nbsp;-startOffset&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(0F&nbsp;,&nbsp;-startLength) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(-startOffset&nbsp;&nbsp;,&nbsp;-startOffset&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(-startLength&nbsp;,&nbsp;0F) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(-startOffset&nbsp;&nbsp;,&nbsp;startOffset&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(0F&nbsp;,&nbsp;startLength) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawPath(path&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;viewBackgroundColor &nbsp;&nbsp;&nbsp;&nbsp;//通过缩小绘制星星内部形状 &nbsp;&nbsp;&nbsp;&nbsp;canvas.scale(0.3F&nbsp;,&nbsp;0.3F) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawPath(path&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() }星球外部private&nbsp;fun&nbsp;drawGas(canvas:&nbsp;Canvas,&nbsp;index:&nbsp;Float)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;canvas.rotate(45F) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;gasWidth&nbsp;=&nbsp;18F &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;baseR&nbsp;=&nbsp;baseR&nbsp;*&nbsp;0.7F &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;absBaseR&nbsp;=&nbsp;baseR&nbsp;/&nbsp;5F &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paint&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;paint.strokeWidth&nbsp;=&nbsp;gasWidth &nbsp;&nbsp;&nbsp;&nbsp;paint.style&nbsp;=&nbsp;Paint.Style.STROKE &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;0xff2F3768.toInt() &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paintArc&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;paintArc.color&nbsp;=&nbsp;0xff2F3768.toInt() &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;gasLength&nbsp;=&nbsp;baseR&nbsp;*&nbsp;2F &nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;gsaL&nbsp;=&nbsp;gasWidth&nbsp;/&nbsp;2F&nbsp;*&nbsp;3 &nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;maxGasLength&nbsp;=&nbsp;(gasLength&nbsp; &nbsp;gsaL&nbsp;)&nbsp;/&nbsp;2 &nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;index&nbsp;=&nbsp;index &nbsp;&nbsp;&nbsp;&nbsp;canvas.scale(1F&nbsp;,&nbsp;-1F) &nbsp;&nbsp;&nbsp;&nbsp;//绘制星球后面的气流情况 &nbsp;&nbsp;&nbsp;&nbsp;//舍不得那么多定义好的变量 &nbsp;&nbsp;&nbsp;&nbsp;//又不想写个参数很多的函数,就这么实现&nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(baseR&nbsp;,&nbsp;baseR&nbsp;*&nbsp;1.2F) &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(0F&nbsp;,&nbsp;absBaseR) &nbsp;&nbsp;&nbsp;&nbsp;//drawLines函数一个绘制两头带半圆的线段 &nbsp;&nbsp;&nbsp;&nbsp;drawLines(0F,&nbsp;maxGasLength,&nbsp;canvas,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;drawWhite(&nbsp;maxGasLength&nbsp;*&nbsp;index,&nbsp;gasWidth&nbsp;,&nbsp;gsaL&nbsp;*&nbsp;2&nbsp;,&nbsp;canvas) &nbsp;&nbsp;&nbsp;&nbsp;drawWhite(&nbsp;maxGasLength&nbsp;*&nbsp;(index&nbsp;-&nbsp;1&nbsp;)&nbsp;*&nbsp;1.1F,&nbsp;gasWidth&nbsp;,&nbsp;gsaL&nbsp;*&nbsp;2&nbsp;,&nbsp;canvas) &nbsp;&nbsp;&nbsp;&nbsp;drawWhite(&nbsp;maxGasLength&nbsp;*&nbsp;(index&nbsp; &nbsp;1&nbsp;)&nbsp;*&nbsp;1.1F,&nbsp;gasWidth&nbsp;,&nbsp;gsaL&nbsp;*&nbsp;2&nbsp;,&nbsp;canvas) &nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() &nbsp;&nbsp;&nbsp;&nbsp;index&nbsp;=&nbsp;index&nbsp; &nbsp;0.3F &nbsp;&nbsp;&nbsp;&nbsp;//.....没有写函数就不上重复的代码了 &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;rectf&nbsp;=&nbsp;RectF(-baseR&nbsp;,&nbsp;-baseR&nbsp;,&nbsp;baseR&nbsp;,baseR) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectf&nbsp;,&nbsp;0F&nbsp;,&nbsp;180F&nbsp;,&nbsp;false&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawLine(baseR&nbsp;,0F&nbsp;,&nbsp;&nbsp;baseR&nbsp;,&nbsp;&nbsp;-baseR,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawLine(-baseR&nbsp;,0F&nbsp;,&nbsp;&nbsp;-baseR&nbsp;,&nbsp;&nbsp;-baseR,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() } //绘制尾部空白部分 private&nbsp;fun&nbsp;drawWhite(offset:&nbsp;Float,&nbsp;gasWidth:&nbsp;Float,&nbsp;gsaL&nbsp;:&nbsp;Float&nbsp;,&nbsp;canvas:&nbsp;Canvas)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;r&nbsp;=&nbsp;gasWidth&nbsp;/&nbsp;2F &nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(&nbsp;0F&nbsp;,&nbsp;offset&nbsp;-&nbsp;2&nbsp;*&nbsp;gsaL&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;pointPaint&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;pointPaint.strokeWidth&nbsp;=&nbsp;20F &nbsp;&nbsp;&nbsp;&nbsp;pointPaint.color&nbsp;=&nbsp;Color.RED &nbsp;&nbsp;&nbsp;&nbsp;//通过贝塞尔曲线绘制半圆效果 &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;path&nbsp;=&nbsp;Path() &nbsp;&nbsp;&nbsp;&nbsp;path.moveTo(-r&nbsp;,&nbsp;gsaL) &nbsp;&nbsp;&nbsp;&nbsp;path.cubicTo( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;r&nbsp;*&nbsp;C&nbsp;,&nbsp;&nbsp;gsaL&nbsp;-&nbsp;r, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r&nbsp;*&nbsp;C&nbsp;,&nbsp;&nbsp;gsaL&nbsp;-&nbsp;r, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r&nbsp;,&nbsp;gsaL &nbsp;&nbsp;&nbsp;&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(r&nbsp;,&nbsp;-&nbsp;gsaL) &nbsp;&nbsp;&nbsp;&nbsp;path.cubicTo( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r&nbsp;*&nbsp;C&nbsp;,&nbsp;&nbsp;-&nbsp;gsaL&nbsp; &nbsp;r, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-r&nbsp;*&nbsp;C&nbsp;,&nbsp;&nbsp;-&nbsp;gsaL&nbsp; &nbsp;r, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-r&nbsp;,&nbsp;-&nbsp;gsaL &nbsp;&nbsp;&nbsp;&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;path.lineTo(-r&nbsp;,&nbsp;gsaL&nbsp;*&nbsp;1.5F) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paint&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;viewBackgroundColor &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawPath(path&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() }星球private&nbsp;fun&nbsp;drawPlanet(canvas:&nbsp;Canvas&nbsp;,&nbsp;index&nbsp;:&nbsp;Float)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;//设置原图层 &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;srcB&nbsp;=&nbsp;makeSrc(index) &nbsp;&nbsp;&nbsp;&nbsp;//设置遮罩层 &nbsp;&nbsp;&nbsp;&nbsp;//遮罩层只有一和星球大小一样的圆 &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;dstB&nbsp;=&nbsp;makeDst(index) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paint&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;canvas.saveLayer(-baseR,&nbsp;-baseR,&nbsp;baseR&nbsp;,&nbsp;baseR,&nbsp;null,&nbsp;Canvas.ALL_SAVE_FLAG) &nbsp;&nbsp;&nbsp;&nbsp;//绘制遮罩层 &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawBitmap(dstB,&nbsp;&nbsp;-baseR&nbsp;/&nbsp;2F,&nbsp;-baseR&nbsp;/&nbsp;2F&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;//设置遮罩模式为SRC_IN显示原图层中原图层与遮罩层相交部分 &nbsp;&nbsp;&nbsp;&nbsp;paint.xfermode&nbsp;=&nbsp;PorterDuffXfermode(PorterDuff.Mode.SRC_IN) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawBitmap(srcB,&nbsp;width&nbsp;/&nbsp;-2F,&nbsp;height&nbsp;/&nbsp;-2F&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;paint.xfermode&nbsp;=&nbsp;null } //设置源图层 fun&nbsp;makeSrc(index&nbsp;:Float):&nbsp;Bitmap&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;bm&nbsp;=&nbsp;Bitmap.createBitmap(width,&nbsp;height,&nbsp;Bitmap.Config.ARGB_8888) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;canvas&nbsp;=&nbsp;Canvas(bm) &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(width.toFloat()&nbsp;/&nbsp;2F&nbsp;,&nbsp;height.toFloat()&nbsp;/&nbsp;2F) &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paint&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;0xff57BEC6.toInt() &nbsp;&nbsp;&nbsp;&nbsp;paint.style&nbsp;=&nbsp;Paint.Style.FILL &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;rectf&nbsp;=&nbsp;RectF(-baseR&nbsp;/&nbsp;2F,&nbsp;-baseR&nbsp;/&nbsp;2F,&nbsp;baseR&nbsp;/&nbsp;2F,&nbsp;baseR&nbsp;/&nbsp;2F) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectf&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;//绘制星球背景 &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;0xff78D7DE.toInt() &nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;baseR&nbsp;=&nbsp;baseR&nbsp;*&nbsp;0.9.toFloat() &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;rectf2&nbsp;=&nbsp;RectF(-baseR&nbsp;/&nbsp;2F,&nbsp;-baseR&nbsp;/&nbsp;2F,&nbsp;baseR&nbsp;/&nbsp;2F,&nbsp;baseR&nbsp;/&nbsp;2F) &nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(baseR&nbsp;/&nbsp;6F&nbsp;,&nbsp;baseR&nbsp;/&nbsp;6F) &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectf2&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() &nbsp;&nbsp;&nbsp;&nbsp;canvas.rotate(-45F) &nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;bottomBaseR&nbsp;=&nbsp;baseR&nbsp;/&nbsp;0.9F&nbsp;/&nbsp;2 &nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;path&nbsp;=&nbsp;Path() &nbsp;&nbsp;&nbsp;&nbsp;path.moveTo(-bottomBaseR&nbsp;,&nbsp;0F) &nbsp;&nbsp;&nbsp;&nbsp;path.cubicTo(-bottomBaseR&nbsp;,&nbsp;bottomBaseR&nbsp;*&nbsp;2,&nbsp;bottomBaseR&nbsp;&nbsp;,&nbsp;bottomBaseR&nbsp;*&nbsp;2,&nbsp;bottomBaseR&nbsp;,&nbsp;0F) &nbsp;&nbsp;&nbsp;&nbsp;path.cubicTo( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bottomBaseR&nbsp;*&nbsp;C,bottomBaseR&nbsp;, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-bottomBaseR&nbsp;*&nbsp;C,bottomBaseR&nbsp;, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-bottomBaseR&nbsp;,&nbsp;0F &nbsp;&nbsp;&nbsp;&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;//绘制星球背景的阴影效果 &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;0xffAAEEF2.toInt() &nbsp;&nbsp;&nbsp;&nbsp;paint.style&nbsp;=&nbsp;Paint.Style.FILL &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawPath(path&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;//绘制星球的地貌 &nbsp;&nbsp;&nbsp;&nbsp;drawPoints(index&nbsp;,&nbsp;canvas) &nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() &nbsp;&nbsp;&nbsp;&nbsp;paint.strokeWidth&nbsp;=&nbsp;30F &nbsp;&nbsp;&nbsp;&nbsp;paint.color&nbsp;=&nbsp;0xff2F3768.toInt() &nbsp;&nbsp;&nbsp;&nbsp;paint.style&nbsp;=&nbsp;Paint.Style.STROKE &nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectf&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paint) &nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;bm } private&nbsp;fun&nbsp;drawPoints(index:&nbsp;Float,&nbsp;canvas:&nbsp;Canvas)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paintB&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;paintS&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paintS.style&nbsp;=&nbsp;Paint.Style.FILL &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paintS.color&nbsp;=&nbsp;0xffE7F2FB.toInt() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paintB.style&nbsp;=&nbsp;Paint.Style.FILL &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paintB.color&nbsp;=&nbsp;0xff2F3768.toInt() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;baseRB&nbsp;=&nbsp;baseR&nbsp;/&nbsp;2F&nbsp;/&nbsp;3 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;baseRS&nbsp;=&nbsp;baseR&nbsp;/&nbsp;2F&nbsp;/&nbsp;3&nbsp;/&nbsp;3 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;rectfB&nbsp;=&nbsp;RectF(-baseRB,&nbsp;-baseRB,&nbsp;baseRB,&nbsp;baseRB) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;rectfS&nbsp;=&nbsp;RectF(-baseRS,&nbsp;-baseRS,&nbsp;baseRS,&nbsp;baseRS) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;pointPaint&nbsp;=&nbsp;Paint() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pointPaint.color&nbsp;=&nbsp;Color.BLACK &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pointPaint.strokeWidth&nbsp;=&nbsp;50F &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;coverWidth&nbsp;=&nbsp;baseR &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//通过移动坐标原点模拟星球的自转效果 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(-coverWidth&nbsp;/&nbsp;2F&nbsp;,&nbsp;coverWidth&nbsp;*&nbsp;1.5F) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val&nbsp;index&nbsp;=&nbsp;index &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(0F&nbsp;,&nbsp;coverWidth&nbsp;*&nbsp;index&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//重复绘制三次星球的地貌使得星球的自转无缝连接 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;(i&nbsp;in&nbsp;0..2){ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(coverWidth&nbsp;/&nbsp;3F&nbsp;/&nbsp;2&nbsp;&nbsp;,&nbsp;-coverWidth&nbsp;/&nbsp;3F&nbsp;*&nbsp;2) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectfB&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paintB) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectfS&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paintS) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(coverWidth&nbsp;/&nbsp;3F&nbsp;*2&nbsp;,&nbsp;-coverWidth&nbsp;/&nbsp;3F) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectfB&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paintB) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectfS&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paintS) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(coverWidth&nbsp;/&nbsp;3F&nbsp;*2&nbsp;,&nbsp;-coverWidth&nbsp;/&nbsp;8F&nbsp;*&nbsp;7&nbsp; &nbsp;-coverWidth&nbsp;/&nbsp;10F&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectfS&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paintB) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.save() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(coverWidth&nbsp;/&nbsp;3F&nbsp;*2&nbsp;,&nbsp;-coverWidth&nbsp;/&nbsp;8F&nbsp;*&nbsp;7&nbsp;&nbsp;-&nbsp;-coverWidth&nbsp;/&nbsp;10F&nbsp;) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.drawArc(rectfS&nbsp;,&nbsp;0F&nbsp;,&nbsp;360F&nbsp;,&nbsp;true&nbsp;,&nbsp;paintB) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.restore() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;canvas.translate(0F&nbsp;,&nbsp;-coverWidth) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值