Android|Android 仿微信小程序入口动画

目录

  • 效果对比
  • 流程分析
    • 自定义ViewGroup
    • 小程序缩放比例值计算
    • 动画遮罩
    • MainActivity

效果对比 微信原版

仿照效果


流程分析

自定义ViewGroup

整个布局是通过自定义ViewGroup来管理的,在自定义ViewGroup中,子布局一共有两个,一个是小程序布局,一个是会话列表布局,然后按照上下分别摆放就可以了。
Android|Android 仿微信小程序入口动画
文章图片

package com.example.kotlindemo.widget.weixinimport android.content.Contextimport android.content.res.Resourcesimport android.util.AttributeSetimport android.util.Logimport android.view.MotionEventimport android.view.Viewimport android.view.ViewGroupimport androidx.core.view.ViewCompatimport androidx.customview.widget.ViewDragHelperimport com.example.kotlindemo.Rimport java.math.BigDecimalclass WeiXinMainPullViewGroup @JvmOverloads constructor(context: Context?,attrs: AttributeSet? = null,defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) {public var viewDragHelper: ViewDragHelper = ViewDragHelper.create(this, 0.5f, DragHandler()); var headerMaskView: WeiXinPullHeaderMaskView? = nullvar isOpen: Boolean = false; val NAVIGAATION_HEIGHT = 100init {}override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {for (index in 0 until childCount) {if (getChildAt(index) != headerMaskView) {getChildAt(index).layout(l, paddingTop, r, b)}}}override fun computeScroll() {if (viewDragHelper.continueSettling(true)) {ViewCompat.postInvalidateOnAnimation(this); }}override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {Log.i("TAG", "onInterceptTouchEvent: ${ev.action}")MotionEvent.ACTION_MOVEreturn true}override fun onTouchEvent(event: MotionEvent): Boolean {viewDragHelper.processTouchEvent(event)return true}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)measureChildren(widthMeasureSpec, heightMeasureSpec)}fun createMaskView() {if (headerMaskView == null) {headerMaskView = WeiXinPullHeaderMaskView(context, null, 0)addView(headerMaskView)}}inner class DragHandler : ViewDragHelper.Callback() {override fun tryCaptureView(child: View, pointerId: Int): Boolean {return child is WeiXinMainLayout; }override fun onViewDragStateChanged(state: Int) {super.onViewDragStateChanged(state)}/*** 设置进度,设置遮罩layout*/override fun onViewPositionChanged(changedView: View,left: Int,top: Int,dx: Int,dy: Int) {createMaskView(); var programView = getChildAt(0)var divide = BigDecimal(top.toString()).divide(BigDecimal(measuredHeight - NAVIGAATION_HEIGHT),4,BigDecimal.ROUND_HALF_UP)divide = divide.multiply(BigDecimal("100"))divide = divide.multiply(BigDecimal("0.002"))divide = divide.add(BigDecimal("0.8"))if (!isOpen) {programView.scaleX = divide.toFloat()programView.scaleY = divide.toFloat()} else {programView.top = paddingTop + (-((measuredHeight - NAVIGAATION_HEIGHT) - top))}headerMaskView!!.maxHeight = measuredHeight / 3headerMaskView!!.layout(0, paddingTop, measuredWidth, top)headerMaskView!!.setProgress(top.toFloat() / ((measuredHeight - (NAVIGAATION_HEIGHT + paddingTop)) / 3) * 100,measuredHeight - (NAVIGAATION_HEIGHT + paddingTop))if (top == paddingTop) {isOpen = false}if (top == measuredHeight - NAVIGAATION_HEIGHT) {isOpen = true}}override fun onViewCaptured(capturedChild: View, activePointerId: Int) {super.onViewCaptured(capturedChild, activePointerId)var programView = getChildAt(0)programView.top = paddingTop; }/*** 释放*/override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {/*** 如果已经打开或者释放后小于屏幕三分之一,回到原位*/if (isOpen or (releasedChild.top + paddingTop <= measuredHeight / 3)) {viewDragHelper.smoothSlideViewTo(releasedChild, 0, paddingTop); ViewCompat.postInvalidateOnAnimation(this@WeiXinMainPullViewGroup); return}viewDragHelper.smoothSlideViewTo(releasedChild, 0, measuredHeight - NAVIGAATION_HEIGHT); ViewCompat.postInvalidateOnAnimation(this@WeiXinMainPullViewGroup); }override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {if (top <= paddingTop) {return paddingTop}return (child.top + dy / 1.3).toInt(); }}}

还要增加一个用来填充状态栏的View,他的高度是动态获取的,整体布局是RelativeLayout,因为可以方便的设置中间View在状态下面和在导航栏上面。
class ViewUtils {companion object{@JvmStaticfun getStatusBarHeight(resources: Resources): Int {var result = 0val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")if (resourceId > 0) {result = resources.getDimensionPixelSize(resourceId)}return result}}}


小程序缩放比例值计算

然后要做的就是拖动View,可以借助ViewDragHelper来完成,当拖动会话布局的时候,小程序的布局开始做一个缩放比例动画,这个缩放值我在这是这样做的,因为不可能是从0开始,要从一个基础值开始,这个基础值就是0.8,那么剩下0.2的缩放值,就是从开始下拉算起,到整体的高度的百分比。
比如屏幕高度是1000,下拉到500的时候,那么这个缩放值就是0.1,在加上基础值0.8,计算方式如下,整体高度还要减去导航栏的高度。
var divide = BigDecimal(top.toString()).divide(BigDecimal(measuredHeight-NAVIGAATION_HEIGHT), 4, BigDecimal.ROUND_HALF_UP)divide = divide.multiply(BigDecimal("100"))divide = divide.multiply(BigDecimal("0.002" ))divide = divide.add(BigDecimal("0.8"))if (!isOpen) {programView.scaleX = divide.toFloat()programView.scaleY = divide.toFloat()} else {programView.top = paddingTop + (-((measuredHeight - NAVIGAATION_HEIGHT) - top))}

这里就注意细节了,下拉的时候,小程序布局是通过缩放呈现的,但是上滑关闭的时,小程序布局是和会话布局同时向上走的。

动画遮罩

这是比较麻烦的一步,就是绘制进度动画,也就是那三个圆点。
这个原点有三种状态,一是出现时从小到大,二是到一定大小后,分离出两个固定大小的圆,但是这两个圆比此时中间的要小,并且和下拉进度慢慢向两边扩撒,三是中间的圆开始缩小,直到和其余两个圈同等大小。
这里就要另一波细节了,当还在屏幕的三分之一下拉时,这个头部遮罩布局整体还是不透明的,但是到屏幕的三分之一时,这个布局的透明度开始从255到0运动。并且到达三分之一的时候,还要振动一下,并且只要振动过了,那么在手指未松开时,再次到达屏幕的三分之一时,不会产生振动。
还有一波细节,状态栏由于使用了View填充,所以,从屏幕三份之一后开始,这个View的透明度也要从255-0开始运动。
完整代码如下。
package com.example.kotlindemo.widget.weixinimport android.content.Contextimport android.graphics.Canvasimport android.graphics.Colorimport android.graphics.Paintimport android.os.VibrationEffectimport android.os.Vibratorimport android.util.AttributeSetimport android.util.Logimport android.view.Viewimport androidx.core.content.ContextCompatimport com.example.kotlindemo.MainActivityimport com.example.kotlindemo.Rclass WeiXinPullHeaderMaskView @JvmOverloads constructor(context: Context?,attrs: AttributeSet?,defStyleAttr: Int) :View(context, attrs, defStyleAttr) {var isVibrator: Boolean = false; var progress: Int = 0; var maxHeight: Int = 0; private val CIRCLE_MAX_SIZE = 32; var parentHeight=0; var paint = Paint()private val DEFAULT_CIRCLE_SIZE=8f; init {setBackgroundColor(Color.argb(255 , 239, 239, 239))paint.alpha=255; paint.color = ContextCompat.getColor(context!!, R.color.circleColor)paint.isAntiAlias = true; }override fun onDraw(canvas: Canvas) {super.onDraw(canvas)var value = https://www.it610.com/article/height.toFloat() / maxHeightif (height <= maxHeight / 2) {canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), CIRCLE_MAX_SIZE * value, paint)} else {if (progress<100){var diff = (value - 0.5f) * CIRCLE_MAX_SIZEcanvas.drawCircle(((width / 2).toFloat()-((0.4f-value)*100)), (height / 2).toFloat(), DEFAULT_CIRCLE_SIZE, paint)canvas.drawCircle(((width / 2).toFloat()+((0.4f-value)*100)), (height / 2).toFloat(), DEFAULT_CIRCLE_SIZE, paint)if ((CIRCLE_MAX_SIZE * 0.5f) - diff<=DEFAULT_CIRCLE_SIZE){canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), DEFAULT_CIRCLE_SIZE, paint)}else{canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), (CIRCLE_MAX_SIZE * 0.5f) - diff, paint)}}else{paint.alpha=getAlphaValue(); canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), DEFAULT_CIRCLE_SIZE, paint)canvas.drawCircle((width / 2).toFloat()-((0.4f)*100), (height / 2).toFloat(), DEFAULT_CIRCLE_SIZE, paint)canvas.drawCircle((width / 2).toFloat()+(((0.4f)*100)), (height / 2).toFloat(), DEFAULT_CIRCLE_SIZE, paint)}}}private fun getAlphaValue():Int{val dc=parentHeight/3-ViewUtils.getStatusBarHeight(resources); val alpha=((height).toFloat()-dc)/(parentHeight-(dc))return 255-(255*alpha).toInt()}private fun vibrator() {var vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibratorif (android.os.Build.VERSION.SDK_INT>= android.os.Build.VERSION_CODES.O) {var createOneShot = VibrationEffect.createOneShot(7, 255)vibrator.vibrate(createOneShot)} else {vibrator.vibrate(7)}}fun setProgress(value: Float,parentHeight:Int) {this.progress = value.toInt(); this.parentHeight=parentHeight; if (value >= 100 && !isVibrator) {vibrator()isVibrator = true; }if (value < 100) {isVibrator = false; }if (progress>=100){setBackgroundColor(Color.argb(getAlphaValue() , 239, 239, 239))var mainActivity = context as MainActivitymainActivity.changeStatusBackgroundAlphaValue(getAlphaValue())}else{setBackgroundColor(Color.argb(255, 239, 239, 239))}invalidate()}}

还有就是这三个原点是始终位于遮罩View中间的,绘制的时候只需要在中间绘制,遮罩View的高度会被外界View所更改。

MainActivity
import android.graphics.Colorimport android.os.Buildimport android.os.Bundleimport android.view.Viewimport android.view.Windowimport androidx.appcompat.app.AppCompatActivityimport androidx.databinding.DataBindingUtilimport com.example.kotlindemo.databinding.ActivityMainBindingimport com.example.kotlindemo.widget.weixin.ChatSessionimport com.example.kotlindemo.widget.weixin.ChatSessionAdapterimport com.example.kotlindemo.widget.weixin.ViewUtilsclass MainActivity : AppCompatActivity() {lateinit var binding: ActivityMainBinding; fun changeStatusBackgroundAlphaValue(value: Int){binding.statusBar.setBackgroundColor(Color.argb(value, 239, 239, 239))}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this, R.layout.activity_main); var layoutParams = binding.statusBar.layoutParamslayoutParams.height=ViewUtils.getStatusBarHeight(resources)binding.statusBar.layoutParams=layoutParamsbinding.wxMain.setPadding(0, ViewUtils.getStatusBarHeight(resources), 0, 0)if (Build.VERSION.SDK_INT >= 21) {val window: Window = windowwindow.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREENor View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)window.setStatusBarColor(Color.TRANSPARENT)}val chatSessions= mutableListOf()for (index in 0 .. 10){chatSessions.add(ChatSession("https://img2.baidu.com/it/u=3538084390,1079314259&fm=26&fmt=auto&gp=0.jpg","马云","你来,我把公司给你","上午"))chatSessions.add(ChatSession("https://img0.baidu.com/it/u=273576249,1042072491&fm=26&fmt=auto&gp=0.jpg","奥巴马","哥哥在哪呢","上午"))chatSessions.add(ChatSession("https://img1.baidu.com/it/u=152902017,4157746361&fm=11&fmt=auto&gp=0.jpg","成龙","马上接你","上午"))chatSessions.add(ChatSession("https://img0.baidu.com/it/u=3789809038,289359647&fm=26&fmt=auto&gp=0.jpg","窃瓦辛格","我教你啊","上午"))}binding.chatList.adapter=ChatSessionAdapter(chatSessions,this)}}


【Android|Android 仿微信小程序入口动画】以上就是Android 仿微信小程序入口动画的详细内容,更多关于Android 微信小程序入口动画的资料请关注脚本之家其它相关文章!

    推荐阅读