实现类知乎android客户端关注和取消关注的按钮点击效果

例行广告,推广一下我的博客http://zwgeek.com

前端时间在看Android各个客户端上比较出色的动画效果,发现两个动画做的很好的客户端,一个是豌豆荚,一个是知乎。接下来我可能会对这两个客户端的各种效果进行模仿实现。首先让我们看知乎的关注按钮点击效果,关注按钮点击后会有一层遮挡,从你点击的位置慢慢扩散开来,然后变成被点击状态,感觉非常赞。这篇文章从以下几个方面讨论这个效果。

  • Android中实现类似效果的几种方式
    • 用Ripple实现类似效果
    • 用Paint画出类似效果
  • 反编译知乎客户端代码
  • 实现最终效果

先说明一下,项目代码已上传至github,不想看长篇大论的也可以先去下代码,对照代码,哪里不懂点哪里。

代码在这

https://github.com/zgzczzw/ZHFollowButton

首先,让我们我先详细观察了一些知乎的效果,其中有一个很神奇的地方,如图:

注意看第二张图,这个圆形在扩散的时候,圆形底下的字还在,而且新的字也在圆形上,就这个效果实现起来最难。

Android中实现类似效果的几种方式

用Ripple实现类似效果

ripple即波纹效果,是Android API 21以后引入的一种material design的元素,是触摸反馈的一种,也就是说点击的时候会出现水波扩散的样式,demo(见最后)中第一个按钮就是用了ripple效果。

实现方式很简单,实现一个这样的drawable

第一个color是波纹颜色,item里面指定background正常的颜色,可以是一个shape,也可以是一个drawable,还可以是一个selector。

设置为按钮的background即可

如果整个程序的theme用了meterial,那基本所有的带点击效果的控件,比如button都自带这个波纹效果。不过需要注意的是这一套API是21以后才提供的,所以需要做兼容处理。

效果如下:

从图中可以看出即使我设置了波纹为红色(#FF0000),点击后的效果也是淡红色,我猜测因为是水波纹效果,为了不影响按钮本身展示的内容,android系统自动做了透明度的处理,另外从图中也可以明显的看出,水波纹和显示的内容是上下两层的,互不影响,水波纹是在background层面上。这个效果做普通的点击反馈还不错,但绝对实现不出知乎这种用波纹刷新出内容的效果。所以很容易能看出知乎的点击效果不是用ripple做出来的。

用Paint画出类似效果

可能很多人看到知乎关注按钮的效果后,想到的第一种实现方式就是这个,用 paint在点击的地方画圆形,然后让画的圆形半径慢慢变大,实现出扩散出去的样式,我实现了一下,代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mShouldDoAnimation) {
        mMaxRadius = getMeasuredWidth() + 50;
        if (mRevealRadius > mMinBetweenWidthAndHeight / 2)
            mRevealRadius += mRevealRadiusGap * 4;
        else
            mRevealRadius += mRevealRadiusGap;//半径变大
        Paint mPaint = new Paint();
        if (!mIsPressed) {
            mPaint.setColor(Color.WHITE);
        } else {
            mPaint.setColor(Color.RED);
        }//设置画笔颜色
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);

        if (mRevealRadius <= mMaxRadius) {
            //一定时间后再刷新
            postInvalidateDelayed(INVALIDATE_DURATION);
        } else {
            if (mIsPressed) {
                setTextColor(Color.WHITE);
                this.setBackgroundColor(Color.RED);
            } else {
                setTextColor(Color.BLACK);
                this.setBackgroundColor(Color.WHITE);
            }
            mShouldDoAnimation = false;
            invalidate();
        }
    }
}

效果如图:

本来觉得差不多就是这样,但是跟知乎的效果比较一下,还是能发现差别的。用paint画圆能实现的是在点击的地方画一个圆,然后半径慢慢变大慢慢扩散。但是问题在于,画的这个圆会盖住显示的内容,而且画的圆上也不能显示内容。我试过用drawText,也实现不了字和圆一起的效果,解决方法只有,

  • 画的过程中改背景色和上面文字。
  • 然后,画完圆之后把圆擦掉,把下面的背景色和文字显示出来。

这样就会出现一次文字闪烁的问题,首先文字会消失掉,然后画完圆之后才显示出来。因为圆在扩散的时候是看不到文字的,只有等圆消失了,文字才能显示出来。而知乎的效果是文字和圆一起刷出来,而且底下的文字还在,中间也没有文字闪烁的问题,整个过程行云流水,看起来很顺畅,好像用圆形揭开了幕布一样。

综上所述,知乎不是用这两种方式实现的,其实如果不是我自己实现了一下,真的以为第二种方法就是知乎采用的,但是目前看来,很遗憾,知乎采用了一种更好的方式来实现这个效果。

那怎么办呢,我也没什么思路,怎么才能在画圆的时候把字也画在圆上,然后圆下面的背景也还有呢。没什么思路,看看知乎的代码吧,反编译。

反编译知乎代码

反编译的过程我简单说一下:

到知乎官网下载最新的知乎apk
用apktool反编译apk,得到资源文件

在资源文件中搜索follow,这里一开始我搜的是ripple,因为我觉得这个效果总归应该和ripple有关,没结果,于是搜了follow,没想到还真搜出来了。

RevealFollowButton这明显就是我们要的波纹展开的控件,这就好说了,下一步就是去代码里找到这个控件了。这里要记一下,这个控件的位置com.zhihu.android.app.ui.widget.RevealFollowButton

反编译代码
将apk改名成rar,打开,可以找到里面的class文件

知乎用了multidex,所以会有两个class文件,都拖出来放在dex2jar里反编译一下,就能生成两个jar包了,把jar包放在GUI里看一下,就能看到代码了,虽然代码被混淆过,但是基本逻辑还是能看出来的。

知乎实现原理

然后根据前面xml里的路径找到RevelFollowButton的位置,打开代码看就可以了。

这是类的继承关系,RevealFollowButton继承自RevealFrameLayout,然后继承自ZHFrameLayout,这个ZHFrameLayout的父类就是FrameLayout了,从名字我们能看出,RevelFollowButton和RevealFrameLayout就是这个效果实现的两个类了。

看到这个效果的实现是基于Framelayout,我就知道我们之前讨论的方法其实都走错了方向,如果告诉你用framelayout来实现这个效果,你会怎么做?

我的想法是加入两个TextView到这个layout里,然后一个Visible一个gone,如此切换,后来看过代码后,也证明我的这个想法是对的。

看,这里有两个TextView。如此的话,其实切换TextView是很容易实现的,问题是怎么实现波纹切换的效果,那第一件事就是看onDraw函数了,对于GroupView来说是drawChild方法。

RevealFollowButton的drawChild方法没什么内容,基本是调用了父类,那么我们来看RevealFrameLayout的drawChild方法。

这里有两部分逻辑,如果满足一个条件,就做第一部分,一开始我也不知道这个条件是什么,混淆后的代码能看懂大逻辑,像这种小逻辑只能走一步看一步了。所以假设这个条件永远false吧,看第二部分,看到这里瞬间明白了,原来是采用切割画布的方式,把画布切成一个圆的,就能做到显示的内容也在圆上,而不是内容被覆盖在圆下面了。然后同理,把这个圆形区域不断扩大,然后不断刷新,就是实现波形刷出内容的效果了。代码如下吧


protected boolean drawChild(Canvas canvas, View paramView, long paramLong) {
    int i = canvas.save();
    mPath.reset();
    //mCenterX mCenterY是点击的位置,在onTouchEvent里设置
    //mRevealRadius是圆的半径,会渐渐变大
    mPath.addCircle(mCenterX, mCenterY, mRevealRadius, Path.Direction.CW);
    canvas.clipPath(this.mPath);
    boolean bool2 = super.drawChild(canvas, paramView, paramLong);
    canvas.restoreToCount(i);
    return bool2;
}

按照上面说的,肯定还有一个类似于定时器的东西,能不断改变圆形的半径,然后刷新,其实这个在代码里找找很容易就找到了。RevealFrameLayout里除了这个drawChild,没有别的代码了。所以我们来看RevealFollowButton。

RevealFollowButton里面跟定时器有关的就是这句了

一个Animator对象,其实这句代码我是没看懂的,但逻辑很简单,设置一个Animator,定时500ms,在这个过程中修改圆形半径,然后刷新。

Math.hypot(getWidth(), getHeight()))

其中这个方法是根据勾股定理获取三角形的斜边长度,想想我们所要绘制的圆形半径最长是多少,没错,就是TextView的对角线长度。所以,整个逻辑就很简单了。

我搞了下代码,就这样吧

整个方法的代码如下吧,还包括控制FollowTv和unFollowTv哪个显示

protected void setFollowed(boolean isFollowed, boolean needAnimate) {
    mIsFollowed = isFollowed;
    if (isFollowed) {
        mUnFollowTv.setVisibility(View.VISIBLE);
        mFollowTv.setVisibility(View.VISIBLE);
        mFollowTv.bringToFront();
    } else {
        mUnFollowTv.setVisibility(View.VISIBLE);
        mFollowTv.setVisibility(View.VISIBLE);
        mUnFollowTv.bringToFront();
    }
    if (needAnimate) {
        ValueAnimator animator = ObjectAnimator.ofFloat(mFollowTv, "empty", 0.0F, (float) Math.hypot(getMeasuredWidth(), getMeasuredHeight()));
        animator.setDuration(500L);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mRevealRadius = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }
}

根据当前状态把Follow的Textview或UnFollow的TextView显示出来,然后设置一个定时器不断扩大所要绘制圆的半径,根据这个半径裁剪画布成一个渐渐变大的圆形,然后内容就渐渐显示出来了。

实现最终效果

这个效果实现出来之后,试着运行一下,还不错,但是总觉得有地方不对,于是细细观察,终于发现了,知乎的那个效果在刷新的时候,底下的背景不是白色的,还是之前的状态,比如要变成关注的时候,背景中的未关注还是在的,而我们实现的这个,刷新的时候背景是白色的。

这是知乎的

这是我的

所以还是没有知乎那么行云流水,所以我们是少了什么吗。这时候想起来了,之前在RevealFrameLayout的drawChild里有一个判断条件,当时我们不知道它的逻辑是干什么的,现在看来。那部分逻辑就是处理这个的,画子控件的时候,要画两个,FollowTextView和UnFollowTextView,要随圆形刷出的控件我们采用裁剪画布的方式慢慢画出。那作为背景的另一个控件就不需要慢慢画出,只要完全画出来就行了。所以,猜想这里这个判断条件就是判断当前控件是不是要随圆形刷出的控件,如果不是,就直接画出来就行了。所以修改代码如下:

protected boolean drawChild(Canvas canvas, View paramView, long paramLong) {
    if (drawBackground(paramView)) {
        return super.drawChild(canvas, paramView, paramLong);
    }
    int i = canvas.save();
    mPath.reset();
    mPath.addCircle(mCenterX, mCenterY, mRevealRadius, Path.Direction.CW);
    canvas.clipPath(this.mPath);
    boolean bool2 = super.drawChild(canvas, paramView, paramLong);
    canvas.restoreToCount(i);
    return bool2;
}

判断的方法如下:

private boolean drawBackground(View paramView) {
    if (mIsFollowed && paramView == mUnFollowTv) {
        return true;
    } else if (!mIsFollowed && paramView == mFollowTv) {
        return true;
    }
    return false;
}

至此,整个效果就和知乎完全一样了,刷新过程行云流水,非常赞。效果如下

实现代码已上传至github:

https://github.com/zgzczzw/ZHFollowButton

喜欢这篇文章的朋友可以关注一下我的博客http://zwgeek.com

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: PyTorch是一个基于Python开发的机器学习框架,它拥有丰富的工具和功能,适用于各种任务,包括遥感图像地物分。遥感图像地物分是指通过对遥感图像进行分析和识别,将不同地物分为不同的别,如建筑、道路、植被等。 在PyTorch中实现遥感图像地物分可以遵循以下步骤: 1. 数据准备:首先,需要准备用于训练和评估的遥感图像数据集。可以从公开数据集中获取,或者根据实际需要收集和整理数据集。 2. 数据加载:使用PyTorch的数据加载器,将图像数据集加载到内存中,并对数据集进行预处理,如裁剪、缩放和标准化等。 3. 模型设计:选择适合遥感图像分的模型架构,如卷积神经网络(CNN)。可以使用PyTorch提供的模型库,如ResNet、VGG等,也可以自定义模型。 4. 模型训练:将加载的图像数据集输入到模型中,通过定义损失函数和优化器,使用PyTorch提供的自动求导功能,进行模型训练。可以根据需要设置训练的迭代次数、学习率等超参数,并周期性地评估模型的性能。 5. 模型评估:训练完成后,使用测试集对模型进行评估,计算分精度、查准率、查全率等指标,评估模型的性能。 6. 模型应用:经过训练和评估后,可以使用该模型对新的遥感图像进行分预测。将新的图像输入到模型中,经过前向传播计算,得到图像的预测别。 总而言之,通过PyTorch实现遥感图像地物分可以借助其强大的机器学习功能和便捷的开发环境,快速高效地完成图像分任务。同时,PyTorch还提供了丰富的工具和库,方便用户进行模型设计、训练和评估,并具有良好的可扩展性和灵活性,满足不同用户的需求。 ### 回答2: PyTorch是一个常用的深度学习框架,它提供了丰富的功能和工具,可以用于遥感图像地物分任务的实现。在知乎上,关于PyTorch实现遥感图像地物分的问题,可能会有一些相关的回答。 首先,我们需要准备好用于训练的遥感图像数据集。可以使用公开的遥感图像数据集,或者是自己收集的数据集。数据集应包含不同别的地物图像样本,并且要进行适当的标注。 接下来,我们可以使用PyTorch的数据处理工具,如`torchvision`来加载和预处理图像数据。可以使用`torch.utils.data.Dataset`构建一个自定义的数据集,根据需要对图像进行预处理操作,如缩放、裁剪、归一化等。 然后,我们可以使用PyTorch搭建一个卷积神经网络(CNN)模型,用于图像分任务。可以根据具体的需求选择不同的网络结构,如ResNet、VGG等。可以使用`torch.nn`模块来构建自定义的网络模型,包括卷积层、池化层、全连接层等。 在模型搭建完成后,我们需要定义损失函数和优化器来进行训练。常用的损失函数有交叉熵损失函数(CrossEntropyLoss),可以通过`torch.nn.CrossEntropyLoss`来定义。优化器可以选择Adam、SGD等,可以使用`torch.optim`模块来构建。 接着,我们可以编写训练循环,使用训练数据来迭代训练模型。可以使用`torch.utils.data.DataLoader`来创建一个数据迭代器,方便获取批量的数据样本。然后,依次将数据输入到模型中,计算损失函数,并通过优化器来更新模型参数。 在训练过程中,可以使用一些技巧来提高模型性能,如数据增强、学习率调整等。可以通过`torchvision.transforms`来实现数据增强操作,如随机裁剪、随机旋转等。可以使用学习率调整器(Learning Rate Scheduler)来动态调整学习率,如StepLR、ReduceLROnPlateau等。 最后,在训练完成后,我们可以使用测试数据对模型进行评估。可以使用测试数据集来验证模型的泛化能力,并计算评估指标,如准确率、召回率等。 总之,使用PyTorch实现遥感图像地物分是一个相对复杂的任务,但通过合理的数据处理、模型搭建和优化方法,可以有效实现知乎上也有很多关于这一问题的讨论和分享,可以帮助我们更好地理解和实践相关内容。 ### 回答3: pytorch是一个常用的深度学习框架,可以用于遥感图像地物分任务的实现。在pytorch中,可以利用卷积神经网络(CNN)进行图像分任务。 首先,需要准备好遥感图像的数据集。数据集应包含标注好的遥感图像样本,以及每个样本对应的地物分标签。接下来,可以利用pytorch的数据加载工具,如torchvision库中的datasets模块,将数据集按照一定的比例划分为训练集、验证集和测试集。 然后,可以利用pytorch的模型来定义一个卷积神经网络模型。模型的结构可以根据具体任务进行设计,一般建议包含多个卷积层、池化层和全连接层。可以根据需要,使用不同的卷积核大小、步幅和激活函数等。 在模型定义好后,可以利用pytorch的优化器定义一个优化器,如Adam优化器。优化器可以控制模型的权重更新方式,在训练过程中调整学习率和动量等超参数。 接下来,可以利用pytorch的训练循环来训练模型。训练循环包括多个迭代的训练阶段,每个阶段包括前向传播、计算损失、反向传播和模型权重更新等步骤。可以利用pytorch的损失函数定义一个损失函数,如交叉熵损失函数。在训练过程中,通过最小化损失函数来优化模型的权重。 在训练结束后,可以利用验证集来评估模型的性能,并根据需要进行调参和优化。最后,可以利用测试集对训练好的模型进行评估,并根据评估结果进行后续的地物分任务。 总之,pytorch可以提供一个灵活、高效的深度学习框架,用于实现遥感图像地物分任务。通过合理设计模型结构、选择合适的优化器和损失函数,以及使用训练循环和数据加载工具等功能,可以实现高准确率的地物分模型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值