仿Android新版手机QQ底部动态按钮效果

本文介绍了一种仿写新版手机QQ底部动态Tab按钮的方法,通过解析APK资源并使用自定义View实现按钮的动态变形效果,适用于Android应用UI设计与开发。

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

640?wx_fmt=gif

热文导读 | 点击标题阅读

金九银十跳槽季如何进阶找到合适满意的工作?

面试过 3000 位候选人,我发现混得好的年轻人都有这 3 个特质

老程序员被裁员,面试半年未果,同行:都四十了还是码农,怪谁?

作者:大头呆 (完整源码下载见文末

来源:http://www.apkbus.com/blog-963414-78933.html

不知道细心的同学注意到没有,新版手机QQ的底部tab按钮特别有意思,当手放在上面的时候,这个按钮可以随手指的移动而改变形状,如下图:

640?wx_fmt=gif

于是我就仿写了一个,其实实现原理也挺简单的,最终的效果图如下:

640?wx_fmt=gif

第一步我们得下载QQ的apk文件,把里面的按钮图片资源解压出来,不过解压之后一看瞬间懵逼了,居然有这么多文件夹,而且还是混淆过后的:

640?wx_fmt=png

还好机智的我试着搜索tab、menu、selected等关键词后才其中一个文件夹发现了这些图片:

640?wx_fmt=png

原来每个tab按钮都是由1~2张图片拼在一起的,那我就封装成一个控件,然后仿写第一个按钮好了。

实现原理很简单,继承FrameLayout,添加1~2张图片,在onTouchEvent方法中移动。当然为了实现上述效果,两张图片移动的比例(或者阻尼值)是不同的,而且还要限制两张图片的移动范围。

    @Override
    public boolean onTouchEvent(MotionEvent event
{
    float x = event.getX();
    float y = event.getY();
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            changeWhenMove(x, y);
            return true;
        }
        case MotionEvent.ACTION_UP: {
            restorePosition();
            if (isContain(thisevent.getRawX(), event.getRawY())) {
                setHasClick(!hasClick);
                if (OnMenuClickListener != null) {
                    OnMenuClickListener.onItemClick(this);
                }
            }
            return true;
        }
    }
    return true;
}

核心代码如上:ACTION_MOVE时移动控件,ACTION_UP时还原至原位置。如果手指抬起的位置在按钮范围内则响应点击事件。这样就简单实现了QQ动态按钮的效果。实际使用也很简单:简单在布局文件中声明,并在代码中设置普通状态的图片id和按下状态的图片的id就行了:

   <com.renny.qqmenu.QQMenu
    android:id="@+id/avater_container"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"/>
   
   QQMenu.setImgages(R.drawable.skin_tab_icon_conversation_normal
                , R.drawable.skin_tab_icon_conversation_selected
                , R.drawable.rvq, R.drawable.rvr);
   QQMenu.setOnMenuClickListener(new QQMenu.OnMenuClickListener() {
            @Override
            public void onItemClick(View view) {
                Toast.makeText(MainActivity.this"Click "+QQMenu.isHasClick(), 
Toast.LENGTH_SHORT).show();
            }
        });

最后贴下github地址,欢迎提交Issues,如果你觉得用的着的话还请点个赞哦。
github:https://github.com/renjianan/QQMenu

想进阿里吗?快加入我们的知识星球吧,如下:

640?wx_fmt=gif

如有收获,欢迎分享 640?wx_fmt=jpeg

「点赞640?「评论 640?wx_fmt=jpeg

 妈妈常教导我,让我养成良好习惯。这样长大才能成为一个有用的人。良好的习惯是尊敬师长这样长大才能成为一个有用的人。良好的习惯是尊敬师长,爱护同学,对人有礼貌;是不粗心,做事情不拖拉;还是爱护公物,不浪费粮食。为什么呢?因为拥有良好习惯,做一个品德高尚的人,懂得尊重别人,才会得到别人的尊重。我要努力地做到这些。我有一些坏习惯,有时候学习很粗心,把一些会做的题做错。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃饭很慢,有的时候还剩饭。我还起床磨蹭,本来应该迅速地穿好衣服,但是,我总是磨磨蹭蹭地,速度很慢。我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!  在上幼儿园以前,我什么也不会干,就连穿衣服也是妈妈给我穿好,就要上幼儿园了,这样可不行,妈妈锻炼我要学会自己穿衣服。   有一天,妈妈把衣服摆在我面前,开始让我自己穿。一开始。我又哭又叫就是不穿,还把衣服扔的满地都是,然后坐在地上开始大哭,等了好长时间,妈妈还是不理我,我只好自己乖乖的把衣服穿好, 一出了房间门,妈妈就笑了起来,再看看我的衣服,毛衣和裤子都穿反了,我赶紧回房间又重新穿了一遍,这次穿好了,拿起外套,可是外套的扣子又扣不上了,扣子可调皮了,好像故意和我作对,我把扣子往扣眼——人类邪恶的根源;爱情——幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话:幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话亲爱的!擦干你的眼泪,至高无上的爱情已经打开了我们的眼界,使我们成了它的崇拜者。是它,





 妈妈常教导我,让我养成良好习惯。这样长大才能成为一个有用的人。良好的习惯是尊敬师长这样长大才能成为一个有用的人。良好的习惯是尊敬师长,爱护同学,对人有礼貌;是不粗心,做事情不拖拉;还是爱护公物,不浪费粮食。为什么呢?因为拥有良好习惯,做一个品德高尚的人,懂得尊重别人,才会得到别人的尊重。我要努力地做到这些。我有一些坏习惯,有时候学习很粗心,把一些会做的题做错。在生活上,也很粗心,有一次早上起床居然穿反了衣服。我吃饭很慢,有的时候还剩饭。我还起床磨蹭,本来应该迅速地穿好衣服,但是,我总是磨磨蹭蹭地,速度很慢。我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!我打算在这学期里,改掉这些坏习惯。早上起来,迅速地穿好衣服,不拖拉。学习不粗心,仔细完成每一道题。吃饭的时候,要很快的把饭吃完,不剩饭。我要从一点一滴做起,逐渐养成良好习惯。我相信自己一定能成为一名品学兼优的好学生!  在上幼儿园以前,我什么也不会干,就连穿衣服也是妈妈给我穿好,就要上幼儿园了,这样可不行,妈妈锻炼我要学会自己穿衣服。   有一天,妈妈把衣服摆在我面前,开始让我自己穿。一开始。我又哭又叫就是不穿,还把衣服扔的满地都是,然后坐在地上开始大哭,等了好长时间,妈妈还是不理我,我只好自己乖乖的把衣服穿好, 一出了房间门,妈妈就笑了起来,再看看我的衣服,毛衣和裤子都穿反了,我赶紧回房间又重新穿了一遍,这次穿好了,拿起外套,可是外套的扣子又扣不上了,扣子可调皮了,好像故意和我作对,我把扣子往扣眼——人类邪恶的根源;爱情——幸福和光明的源泉。我一直在这些思想的舞台上徘徊。突然我发现两个身影从我面前经过,坐在不远的草地上。这是一对从农田那边走过来的青年男女。农田那边有农民的茅舍。在一阵令人伤心的沉默之后,随着一声长叹,我听见从一个肺痨病人的嘴里说出了这样的话:亲爱的!擦干你的眼泪,至高无上的爱情已经打开了我们的眼界,使我们成了它的崇拜者。是它,

你有好的文章想和大家分享欢迎投稿,直接向我投递文章链接即可


最后,国庆福利来了,我们的知识星球已达到1000人了,之前说过到达1000人时将大大幅涨价到169元,为了反馈大家对我们的关注和厚爱,特此维持现价99元最后一天,今天后(今晚 00:00)后将涨到169元,欢迎大家加入我们的知识星球,更多星球信息参见:

如何进阶成为Java和Android架构师?

金九银十跳槽季如何进阶找到合适满意的工作?

说两件事

640?wx_fmt=jpeg

微信扫描或者点击上方二维码领取Android\Python\AI\Java等高级进阶资源

更多学习资料点击下面的“阅读原文”获取

640?wx_fmt=gif

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值