Android——腾讯QQ的Tab按钮动画效果完美实现
最近在用QQ的时候发现了一个有意思的小细节,如图所示:
可以看到Tab按钮都有一个随着用户拖动而转动的特效,一开始被这个效果惊艳到了,QQ还是很细致的,注重细节和用户体验。
于是利用空闲时间实现了这个效果,所有代码均用kotlin实现,项目效果如图所示:
哈哈是不是一模一样呢,完整的实现代码并不长,只有200多行,但是找思路花了一些时间,也遇到过许多弯路,不过最后都还是坚持下来了,实现的思路概括一下:
首先需要两个背景,内背景(笑脸表情图片)和外背景(笑脸轮廓背景图片),通过反编译QQ的包得到了这两个图片资源文件。然后根据view的onTouchListner,分别在DOWN点击的时候触发放大的动画效果(即上图中的选中状态动画),以及在MOVE的时候判断内背景和外背景的运动,都可以算是向着触摸的点偏移,但是内背景的偏移量比外背景图要多(肉眼可以看出来吧..),所以实现的时候只要注意这个点,以及对偏移边缘(轨迹圆)的判断就可以了。
下面介绍一下实现的步骤以及难点:
1.首先是自定义view的布局文件:
<com.ng.ui.view.CentralTractionButtonandroid:id="@+id/ctt_main"android:button="@null"android:layout_width="100dp"android:layout_height="100dp"android:layout_centerHorizontal="true"android:layout_centerVertical="true"android:background="@android:color/transparent"app:normalexternalbackground="@drawable/iv_rb1_bg_normal"app:normalinsidebackground="@drawable/iv_rb1_in_normal"app:selectedexternalbackground="@drawable/iv_rb1_bg_selected"app:selectedinsidebackground="@drawable/iv_rb1_in_selected"app:text="消息"app:textdimension="12sp" />
其中自定义了几个属性,其中分别对应为:
normalexternalbackground - 未选中状态下的外部背景
normalinsidebackground - 未选中状态下的内部背景
selectedexternalbackground - 选中状态下的外部背景
selectedinsidebackground-选中状态下的内部背景
2.在自定义的view中对这些属性做初始化,对应代码如下:
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {val ta = context.obtainStyledAttributes(attrs, R.styleable.ctattrs)text = ta.getString(R.styleable.ctattrs_text)textdimension = ta.getDimension(R.styleable.ctattrs_textdimension, 1f)normalexternalbackground = ta.getResourceId(R.styleable.ctattrs_normalexternalbackground, 0)normalinsidebackground = ta.getResourceId(R.styleable.ctattrs_normalinsidebackground, 0)selectedinsidebackground = ta.getResourceId(R.styleable.ctattrs_selectedinsidebackground, 0)selectedexternalbackground = ta.getResourceId(R.styleable.ctattrs_selectedexternalbackground, 0)//打印所有的属性val count = attrs.attributeCountfor (i in 0..count - 1) {val attrName = attrs.getAttributeName(i)val attrVal = attrs.getAttributeValue(i)LogUtils.d("attrName = $attrName , attrVal = $attrVal")}ta.recycle()init()}
在init方法中,进行图形的Rect初始化:
private fun init() {initPaint()LogUtils.d("-----init-----")//得到组件宽高中的较小值,再/2得到ob的距离if (mHeight > mWidth) mR = mHeight / 2 else mR = mWidth / 2LogUtils.d("ob的距离:" + mR)mr = mR / 2// 背景图绘制区域mExternalDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())//初始化: 75 75 225 225// 中心图绘制区域mInsideDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())// 内外的图形externalBD = resources.getDrawable(normalexternalbackground) as BitmapDrawablemExternalSrcRect = Rect(0, 0, externalBD!!.intrinsicWidth, externalBD!!.intrinsicHeight)insidelBD = resources.getDrawable(normalinsidebackground) as BitmapDrawablemInsideSrcRect = Rect(0, 0, insidelBD!!.intrinsicWidth, insidelBD!!.intrinsicHeight)}
3.在onDraw方法中进行对内外背景的绘制:
override fun onDraw(canvas: Canvas) {super.onDraw(canvas)//暂时画个边框表示范围val bianKuanPaint = Paint()bianKuanPaint.isAntiAlias = truebianKuanPaint.strokeWidth = 2fbianKuanPaint.style = Paint.Style.STROKEbianKuanPaint.color = resources.getColor(R.color.black)canvas.drawRect(0f, 0f, this.width.toFloat(), this.height.toFloat(), bianKuanPaint)//绘制默认状态下背景图val externalBM = externalBD!!.bitmapcanvas.drawBitmap(externalBM, mExternalSrcRect, mExternalDestRect, bmPaint)//绘制默认状态下中心图val insidelBM = insidelBD!!.bitmapcanvas.drawBitmap(insidelBM, mInsideSrcRect, mInsideDestRect, bmPaint)}
可以看到在onDraw中并没有做什么事情,只是绘制图形而已。
4.在onTouchEvent中进行判断:
override fun onTouchEvent(event: MotionEvent): Boolean {//相较于视图的XYvar mx1 = event.xvar my1 = event.yvar mx2 = event.xvar my2 = event.y //需要减掉标题栏高度LogUtils.d("---onTouchEvent---")LogUtils.d(" 点击坐标:$mx1 $my1")when (event.action) {MotionEvent.ACTION_DOWN -> {LogUtils.d("ACTION_DOWN")//TODO 弹动一下的动画效果postInvalidate()}MotionEvent.ACTION_MOVE -> {LogUtils.d("ACTION_MOVE:" + scrollX + " " + scrollY)//判断点击位置距离中心的距离var distanceToCenter = getDistanceTwoPoint(mx1, my1, centerx, centery)var mExternalOffesetLimit = mr / 4var mInsideOffesetLimit = mr / 2//如果区域在轨迹圆内则移动if (distanceToCenter > mExternalOffesetLimit) {//如果点击位置在组件外,则获取点击位置和中心点连线上的一点(该点满足矩形在组件内)为中心作图// oc/oa = od/obvar od = mx1 - centerxvar ob = getDistanceTwoPoint(centerx, centery, mx1, my1)var oc = od / ob * mExternalOffesetLimit// ca/oa = db/obvar db = centery - my1var ac = db / ob * mExternalOffesetLimit//得到ac和oc判断得出a点的位置mx1 = centerx + ocmy1 = centery - acod = mx2 - centerxob = getDistanceTwoPoint(centerx, centery, mx2, my2)oc = od / ob * mInsideOffesetLimit// ca/oa = db/obdb = centery - my2ac = db / ob * mInsideOffesetLimit//得到ac和oc判断得出a点的位置mx2 = centerx + ocmy2 = centery - ac} else {//获得与中点的距离,*2,如图3var ab = my2 - centeryvar bo = mx2 - centerxLogUtils.d("ab:" + ab + " bo:" + bo)mx2 = centerx + 2f * bomy2 = centery + 2f * abdistanceToCenter = getDistanceTwoPoint(mx1, my1, centerx, centery)if (distanceToCenter > mExternalOffesetLimit) {return super.onTouchEvent(event)}}var left: Int = (mx1 - mr).toInt()var right: Int = (mx1 + mr).toInt()var top: Int = (my1 - mr).toInt()var bottom: Int = (my1 + mr).toInt()//更新背景图绘制区域mExternalDestRect = Rect(left, top, right, bottom)left = (mx2 - mr).toInt()right = (mx2 + mr).toInt()top = (my2 - mr).toInt()bottom = (my2 + mr).toInt()//更新中心图绘制区域mInsideDestRect = Rect(left, top, right, bottom)postInvalidate()}MotionEvent.ACTION_UP -> {LogUtils.d("ACTION_UP")//复原背景图绘制区域mExternalDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())//复原中心图绘制区域mInsideDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())postInvalidate()}}LogUtils.d("---end---")return super.onTouchEvent(event)}
其中最复杂的就是在onMonve里的判断了,首先会判断点击的位置距离组件中心点的距离distanceToCenter,如果这个距离大于我指定的轨迹半径(这里取的是外背景图的轨迹圆半径的四分之一,这样的话偏移量就很小了,更接近于QQ的效果)。 如果点击位置大于这个距离,则执行下面的代码:
//如果点击位置在组件外,则获取点击位置和中心点连线上的一点(该点满足矩形在组件内)为中心作图// oc/oa = od/obvar od = mx1 - centerxvar ob = getDistanceTwoPoint(centerx, centery, mx1, my1)var oc = od / ob * mExternalOffesetLimit// ca/oa = db/obvar db = centery - my1var ac = db / ob * mExternalOffesetLimit//得到ac和oc判断得出a点的位置mx1 = centerx + ocmy1 = centery - acod = mx2 - centerxob = getDistanceTwoPoint(centerx, centery, mx2, my2)oc = od / ob * mInsideOffesetLimit// ca/oa = db/obdb = centery - my2ac = db / ob * mInsideOffesetLimit//得到ac和oc判断得出a点的位置mx2 = centerx + ocmy2 = centery - ac
这段代码要结合下图来看:
可以看到是根据b点(点击的位置),等比计算出a点的位置(即内外轨迹圆的圆心点),并进行内外背景图的绘制。
如果如果点击位置小于distanceToCenter,则执行下面的代码:
//获得与中点的距离,*2,如图3var ab = my2 - centeryvar bo = mx2 - centerxLogUtils.d("ab:" + ab + " bo:" + bo)mx2 = centerx + 2f * bomy2 = centery + 2f * abdistanceToCenter = getDistanceTwoPoint(mx1, my1, centerx, centery)if (distanceToCenter > mExternalOffesetLimit) {return super.onTouchEvent(event)}
结合下图:
可以计算出内圆的圆心点的坐标。这里将内圆的横纵坐标偏移量都延长了两倍,以实现内背景图偏移得更快的效果。
最后全部的代码如下:
/*** Created by GG on 2017/11/2.*/
class CentralTractionButton : RadioButton {//四个图片的idprivate var normalexternalbackground: Int = 0private var normalinsidebackground: Int = 0private var selectedinsidebackground: Int = 0private var selectedexternalbackground: Int = 0//文字private var textdimension: Float = 0fprivate var text: String = ""//绘制图形的画笔private var bmPaint: Paint? = null//图形偏移距离private var offsetDistanceLimit: Float = 0.toFloat()//组件宽高private var mWidth: Float = 0.toFloat()private var mHeight: Float = 0.toFloat()//中心点坐标,相较于屏幕private var centerX: Float = 0.toFloat()private var centerY: Float = 0.toFloat()//中心点坐标,相较于组件内private var centerx: Float = 0.toFloat()private var centery: Float = 0.toFloat()constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {val ta = context.obtainStyledAttributes(attrs, R.styleable.ctattrs)text = ta.getString(R.styleable.ctattrs_text)textdimension = ta.getDimension(R.styleable.ctattrs_textdimension, 1f)normalexternalbackground = ta.getResourceId(R.styleable.ctattrs_normalexternalbackground, 0)normalinsidebackground = ta.getResourceId(R.styleable.ctattrs_normalinsidebackground, 0)selectedinsidebackground = ta.getResourceId(R.styleable.ctattrs_selectedinsidebackground, 0)selectedexternalbackground = ta.getResourceId(R.styleable.ctattrs_selectedexternalbackground, 0)//打印所有的属性val count = attrs.attributeCountfor (i in 0..count - 1) {val attrName = attrs.getAttributeName(i)val attrVal = attrs.getAttributeValue(i)LogUtils.d("attrName = $attrName , attrVal = $attrVal")}ta.recycle()init()}override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {super.onLayout(changed, left, top, right, bottom)mWidth = measuredWidth.toFloat()mHeight = measuredHeight.toFloat()LogUtils.d("onLayout: $mWidth $mHeight")//可供位移的距离offsetDistanceLimit = mWidth / 6centerY = ((getBottom() + getTop()) / 2).toFloat()centerX = ((getRight() + getLeft()) / 2).toFloat()centerx = mWidth / 2centery = mHeight / 2LogUtils.d("中心点坐标: $centerX $centerY")init()}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)}//轨迹圆外径的半径mR = obvar mR: Float = 0.toFloat()//背景图图形的半径 = 长宽(这里类似于直径)/2 = ob/2var mr: Float = 0.toFloat()private fun init() {initPaint()LogUtils.d("-----init-----")//得到组件宽高中的较小值,再/2得到ob的距离if (mHeight > mWidth) mR = mHeight / 2 else mR = mWidth / 2LogUtils.d("ob的距离:" + mR)mr = mR / 2// 背景图绘制区域mExternalDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())//初始化: 75 75 225 225// 中心图绘制区域mInsideDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())// 内外的图形externalBD = resources.getDrawable(normalexternalbackground) as BitmapDrawablemExternalSrcRect = Rect(0, 0, externalBD!!.intrinsicWidth, externalBD!!.intrinsicHeight)insidelBD = resources.getDrawable(normalinsidebackground) as BitmapDrawablemInsideSrcRect = Rect(0, 0, insidelBD!!.intrinsicWidth, insidelBD!!.intrinsicHeight)setOnCheckedChangeListener { compoundButton, b ->if (b) {externalBD = resources.getDrawable(selectedexternalbackground) as BitmapDrawableinsidelBD = resources.getDrawable(selectedinsidebackground) as BitmapDrawableval pvhX = PropertyValuesHolder.ofFloat("scaleX", 0.1f,1f)val pvhY = PropertyValuesHolder.ofFloat("scaleY", 0.1f,1f)val objectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY)objectAnimator.duration = 500val overshootInterpolator = OvershootInterpolator(1.2f)objectAnimator.interpolator = overshootInterpolatorobjectAnimator.start()postInvalidate()} else {externalBD = resources.getDrawable(normalexternalbackground) as BitmapDrawableinsidelBD = resources.getDrawable(normalinsidebackground) as BitmapDrawablepostInvalidate()}}}//初始化画笔private fun initPaint() {//绘制图形的画笔bmPaint = Paint()bmPaint!!.isAntiAlias = true//抗锯齿功能bmPaint!!.style = Paint.Style.FILL//设置填充样式 Style.FILL/Style.FILL_AND_STROKE/Style.STROKE}internal var mExternalSrcRect: Rect? = nullinternal var mExternalDestRect: Rect? = nullinternal var mInsideSrcRect: Rect? = nullinternal var mInsideDestRect: Rect? = nullvar externalBD: BitmapDrawable? = nullvar insidelBD: BitmapDrawable? = nulloverride fun onDraw(canvas: Canvas) {super.onDraw(canvas)//暂时画个边框表示范围val bianKuanPaint = Paint()bianKuanPaint.isAntiAlias = truebianKuanPaint.strokeWidth = 2fbianKuanPaint.style = Paint.Style.STROKEbianKuanPaint.color = resources.getColor(R.color.black)canvas.drawRect(0f, 0f, this.width.toFloat(), this.height.toFloat(), bianKuanPaint)//绘制默认状态下背景图val externalBM = externalBD!!.bitmapcanvas.drawBitmap(externalBM, mExternalSrcRect, mExternalDestRect, bmPaint)//绘制默认状态下中心图val insidelBM = insidelBD!!.bitmapcanvas.drawBitmap(insidelBM, mInsideSrcRect, mInsideDestRect, bmPaint)}override fun setOnCheckedChangeListener(listener: CompoundButton.OnCheckedChangeListener) {super.setOnCheckedChangeListener(listener)}override fun onTouchEvent(event: MotionEvent): Boolean {//相较于视图的XYvar mx1 = event.xvar my1 = event.yvar mx2 = event.xvar my2 = event.y //需要减掉标题栏高度LogUtils.d("---onTouchEvent---")LogUtils.d(" 点击坐标:$mx1 $my1")when (event.action) {MotionEvent.ACTION_DOWN -> {LogUtils.d("ACTION_DOWN")//TODO 弹动一下的动画效果postInvalidate()}MotionEvent.ACTION_MOVE -> {LogUtils.d("ACTION_MOVE:" + scrollX + " " + scrollY)//判断点击位置距离中心的距离var distanceToCenter = getDistanceTwoPoint(mx1, my1, centerx, centery)var mExternalOffesetLimit = mr / 4var mInsideOffesetLimit = mr / 2//如果区域在轨迹圆内则移动if (distanceToCenter > mExternalOffesetLimit) {//如果点击位置在组件外,则获取点击位置和中心点连线上的一点(该点满足矩形在组件内)为中心作图// oc/oa = od/obvar od = mx1 - centerxvar ob = getDistanceTwoPoint(centerx, centery, mx1, my1)var oc = od / ob * mExternalOffesetLimit// ca/oa = db/obvar db = centery - my1var ac = db / ob * mExternalOffesetLimit//得到ac和oc判断得出a点的位置mx1 = centerx + ocmy1 = centery - acod = mx2 - centerxob = getDistanceTwoPoint(centerx, centery, mx2, my2)oc = od / ob * mInsideOffesetLimit// ca/oa = db/obdb = centery - my2ac = db / ob * mInsideOffesetLimit//得到ac和oc判断得出a点的位置mx2 = centerx + ocmy2 = centery - ac} else {//获得与中点的距离,*2,如图3var ab = my2 - centeryvar bo = mx2 - centerxLogUtils.d("ab:" + ab + " bo:" + bo)mx2 = centerx + 2f * bomy2 = centery + 2f * abdistanceToCenter = getDistanceTwoPoint(mx1, my1, centerx, centery)if (distanceToCenter > mExternalOffesetLimit) {return super.onTouchEvent(event)}}var left: Int = (mx1 - mr).toInt()var right: Int = (mx1 + mr).toInt()var top: Int = (my1 - mr).toInt()var bottom: Int = (my1 + mr).toInt()//更新背景图绘制区域mExternalDestRect = Rect(left, top, right, bottom)left = (mx2 - mr).toInt()right = (mx2 + mr).toInt()top = (my2 - mr).toInt()bottom = (my2 + mr).toInt()//更新中心图绘制区域mInsideDestRect = Rect(left, top, right, bottom)postInvalidate()}MotionEvent.ACTION_UP -> {LogUtils.d("ACTION_UP")//复原背景图绘制区域mExternalDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())//复原中心图绘制区域mInsideDestRect = Rect((centerx - mr).toInt(), (centery - mr).toInt(),(centerx + mr).toInt(),(centery + mr).toInt())postInvalidate()}}LogUtils.d("---end---")return super.onTouchEvent(event)}//得到两点之间的距离fun getDistanceTwoPoint(x1: Float, y1: Float, x2: Float, y2: Float): Float {return Math.sqrt((Math.pow((x1 - x2).toDouble(), 2.toDouble()) +Math.pow((y1 - y2).toDouble(), 2.toDouble()))).toFloat()}}
Github地址: https://github.com/jiangzhengnan/UI
有什么不懂的可以加我微信问我~~
Android——腾讯QQ的Tab按钮动画效果完美实现相关推荐
- android的动态tab,Android自定义view仿QQ的Tab按钮动画效果(示例代码)
话不多说 先上效果图 实现其实很简单,先用两张图 一张是背景的图,一张是笑脸的图片,笑脸的图片是白色,可能看不出来.实现思路:主要是再触摸view的时候同时移动这两个图片,但是移动的距离不一样,造成的 ...
- Android实现仿QQ登录界面背景动画效果
登录QQ的时候,我们会看到在登录界面的背景不是静态的,而是一段动画效果,刚开始觉得蛮好奇的,现在我们也来实现一下这种效果,实现起来还是挺简单的. 实现步骤: 1.自定义CustomVideoView类 ...
- html5 特效 背景 腾讯,html5腾讯QQ登录界面背景动画特效
特效描述:html5 腾讯QQ 登录界面 背景动画特效.腾讯QQ登陆界面动态背景,直接从腾讯网站获取,js代码有加密,做了个简单地示例 代码结构 1. 引入JS 2. HTML代码 *{margin: ...
- android切换页面上滑动动画,Android ViewPager多页面滑动切换以及动画效果
评论 #28楼[楼主] 2012-06-01 14:27D.Winter @孤寒江雪 我猜 要么在头尾各再加入一个页卡 在页卡切换监听中判断,如果选中了头尾的页卡,就返回到相邻的那个页卡.头尾页卡的界 ...
- 超炫button按钮动画效果
今天从网上看到一个这样的效果,感觉很有创意,自己也搜集了一些资料,仿照着实现了一下. 下面就直接上源码: 首先看一下布局文件: <?xml version="1.0" enc ...
- 简单的UIButton按钮动画效果iOS源码
这个是简单的UIButton按钮动画效果案例,源码,简单的UIButton按钮动画,可以自定义button属性. 效果图: <ignore_js_op> 使用方法: 使用时把ButtonA ...
- android炫酷动画代码,Android高级UI特效仿直播点赞动画效果
Android高级UI特效仿直播点赞动画效果 发布时间:2020-10-02 16:06:18 来源:脚本之家 阅读:117 作者:mrr 本文给大家分享高级UI特效仿直播点赞效果-一个优美炫酷的点赞 ...
- 【每日一练】68—CSS实现一组渐变按钮动画效果
在之前,我们也练习过一些按钮动画的效果,今天我们再来练习一组CSS实现的按钮动画效果,下面是今天练习的最终效果: 接下来,我们再来看一下这个案例的源码. HTML代码: <!doctype ht ...
- CSS特效(二):利用html和css制作毛玻璃特效和按钮动画效果
最终的效果图片: 毛玻璃效果:在style标签中,在form表单的before中利用filter的blur属性以及box-shadow的值设置,就可以做出form表单后面的毛玻璃效果背景,还要记得设置 ...
最新文章
- windows7 ORA-12514 TNS 监听程序当前无法识别连接描述符中请求服务 的解决方法
- Gartner:到2020年人工智能将创造出230万个工作岗位
- getRealPath(““)与getRealPath(“/“)区别及用法——计算机网络相关学习笔记
- python十分钟教程_简洁的十分钟Python入门教程
- zabbix监控Linux系统服务
- 你整明白了吗?Linux Shell 中各种括号的作用 ()、(())、[]、[[]]、{}
- 基于springboot+vue的旅游信息(旅游线路)网站(前后端分离)
- 机器学习算法之KNN算法
- JavaScript数组实现图片轮播
- Hive安装详细步骤
- libiconv android编译,编译cBPM-android-19—CodeBlocks—CentOS7— ndk10—编译libiconv和xerces-c...
- python中day_python day02
- 云服务器初始化失败怎么办,提示交互式登录进程初始化失败是什么原因?解决方法步骤教程...
- 数据报表体系搭建流程
- There is no getter for property named ‘keyword‘ in ‘class cn.wolfcode.qo.Subentry‘] with root caus
- gmail 无法登录 原因解决
- 11 个最佳免费安全网站
- 2021-11-09 Cynthia XSS
- 做一个蓝色的我,有海的辽阔,有天的色泽,有浪漫的裙褶,有纯洁的底色
- 2017奇虎360春招笔试编程
热门文章
- 最简单的基于Flash的流媒体示例:网页播放器(HTTP,RTMP,HLS)
- 计算机权限删除文件win10,win10系统使用管理员权限无法删除部分文件的详细步骤...
- Leap Motion开发第一步环境配置
- matlab 棋盘格畸变矫正
- Spring Boot,Whitelabel Error Page解决方法
- win10安装TeamView 提示rollback framework could not be initialized
- oracle 按天数 均值,oracle 按天数统计数据
- unity cardboard 导出
- 百度地图开发(3)实现本地两点间步行导航
- 数学中奇妙的“金蝉脱壳”(转)