Android 仿微信小程序入口動畫
效果對比
微信原版
仿照效果
流程分析
自定義ViewGroup
整個布局是通過自定義ViewGroup來管理的,在自定義ViewGroup中,子布局一共有兩個,一個是小程序布局,一個是會話列表布局,然后按照上下分別擺放就可以了。
package com.example.kotlindemo.widget.weixin import android.content.Context import android.content.res.Resources import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.core.view.ViewCompat import androidx.customview.widget.ViewDragHelper import com.example.kotlindemo.R import java.math.BigDecimal class 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? = null var isOpen: Boolean = false; val NAVIGAATION_HEIGHT = 100 init { } 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_MOVE return 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) } /** * 設(shè)置進度,設(shè)置遮罩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 / 3 headerMaskView!!.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) { /** * 如果已經(jīng)打開或者釋放后小于屏幕三分之一,回到原位 */ 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(); } } }
還要增加一個用來填充狀態(tài)欄的View,他的高度是動態(tài)獲取的,整體布局是RelativeLayout,因為可以方便的設(shè)置中間View在狀態(tài)下面和在導(dǎo)航欄上面。
class ViewUtils { companion object{ @JvmStatic fun getStatusBarHeight(resources: Resources): Int { var result = 0 val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") if (resourceId > 0) { result = resources.getDimensionPixelSize(resourceId) } return result } } }
小程序縮放比例值計算
然后要做的就是拖動View,可以借助ViewDragHelper來完成,當拖動會話布局的時候,小程序的布局開始做一個縮放比例動畫,這個縮放值我在這是這樣做的,因為不可能是從0開始,要從一個基礎(chǔ)值開始,這個基礎(chǔ)值就是0.8,那么剩下0.2的縮放值,就是從開始下拉算起,到整體的高度的百分比。
比如屏幕高度是1000,下拉到500的時候,那么這個縮放值就是0.1,在加上基礎(chǔ)值0.8,計算方式如下,整體高度還要減去導(dǎo)航欄的高度。
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)) }
這里就注意細節(jié)了,下拉的時候,小程序布局是通過縮放呈現(xiàn)的,但是上滑關(guān)閉的時,小程序布局是和會話布局同時向上走的。
動畫遮罩
這是比較麻煩的一步,就是繪制進度動畫,也就是那三個圓點。
這個原點有三種狀態(tài),一是出現(xiàn)時從小到大,二是到一定大小后,分離出兩個固定大小的圓,但是這兩個圓比此時中間的要小,并且和下拉進度慢慢向兩邊擴撒,三是中間的圓開始縮小,直到和其余兩個圈同等大小。
這里就要另一波細節(jié)了,當還在屏幕的三分之一下拉時,這個頭部遮罩布局整體還是不透明的,但是到屏幕的三分之一時,這個布局的透明度開始從255到0運動。并且到達三分之一的時候,還要振動一下,并且只要振動過了,那么在手指未松開時,再次到達屏幕的三分之一時,不會產(chǎn)生振動。
還有一波細節(jié),狀態(tài)欄由于使用了View填充,所以,從屏幕三份之一后開始,這個View的透明度也要從255-0開始運動。
完整代碼如下。
package com.example.kotlindemo.widget.weixin import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.os.VibrationEffect import android.os.Vibrator import android.util.AttributeSet import android.util.Log import android.view.View import androidx.core.content.ContextCompat import com.example.kotlindemo.MainActivity import com.example.kotlindemo.R class 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 = height.toFloat() / maxHeight if (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_SIZE canvas.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 Vibrator if (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 MainActivity mainActivity.changeStatusBackgroundAlphaValue(getAlphaValue()) }else{ setBackgroundColor(Color.argb(255, 239, 239, 239)) } invalidate() } }
還有就是這三個原點是始終位于遮罩View中間的,繪制的時候只需要在中間繪制,遮罩View的高度會被外界View所更改。
MainActivity
import android.graphics.Color import android.os.Build import android.os.Bundle import android.view.View import android.view.Window import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import com.example.kotlindemo.databinding.ActivityMainBinding import com.example.kotlindemo.widget.weixin.ChatSession import com.example.kotlindemo.widget.weixin.ChatSessionAdapter import com.example.kotlindemo.widget.weixin.ViewUtils class 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<ActivityMainBinding>(this, R.layout.activity_main); var layoutParams = binding.statusBar.layoutParams layoutParams.height=ViewUtils.getStatusBarHeight(resources) binding.statusBar.layoutParams=layoutParams binding.wxMain.setPadding(0, ViewUtils.getStatusBarHeight(resources), 0, 0) if (Build.VERSION.SDK_INT >= 21) { val window: Window = window window.getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR ) window.setStatusBarColor(Color.TRANSPARENT) } val chatSessions= mutableListOf<ChatSession>() 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) } }
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> </data> <RelativeLayout android:background="@drawable/program_background" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <com.example.kotlindemo.widget.weixin.WeiXinMainPullViewGroup android:paddingTop="40dp" android:layout_above="@+id/navigation" android:id="@+id/wx_main" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.kotlindemo.widget.weixin.WeiXinProgram android:paddingLeft="30dp" android:paddingRight="30dp" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:textSize="17sp" android:textColor="#C8C8C8" android:gravity="center" android:text="最近" android:layout_width="match_parent" android:layout_height="40dp"></TextView> <androidx.cardview.widget.CardView android:background="#424459" app:cardBackgroundColor="#424459" app:cardElevation="0dp" app:cardCornerRadius="8dp" android:layout_width="match_parent" android:layout_height="46dp"> <LinearLayout android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:textSize="15sp" android:textColor="#C8C8C8" android:text="搜索小程序" android:gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView> </LinearLayout> </androidx.cardview.widget.CardView> <com.example.kotlindemo.widget.weixin.ProgramGridLayout android:layout_marginTop="20dp" android:layout_width="match_parent" android:layout_height="wrap_content"> </com.example.kotlindemo.widget.weixin.ProgramGridLayout> <com.example.kotlindemo.widget.weixin.ProgramGridLayout android:layout_marginTop="20dp" android:layout_width="match_parent" android:layout_height="wrap_content"> </com.example.kotlindemo.widget.weixin.ProgramGridLayout> </com.example.kotlindemo.widget.weixin.WeiXinProgram> <com.example.kotlindemo.widget.weixin.WeiXinMainLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="44dp" android:background="@color/navigation_color"> <TextView android:textStyle="bold" android:textSize="16sp" android:textColor="#000000" android:layout_centerInParent="true" android:gravity="center" android:text="微信(323)" android:layout_width="wrap_content" android:layout_height="match_parent"></TextView> <ImageView android:layout_marginRight="45dp" android:scaleType="center" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:src="@drawable/ic_search" android:layout_width="28dp" android:layout_height="28dp"></ImageView> <ImageView android:layout_marginRight="10dp" android:scaleType="center" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:src="@drawable/ic_add" android:layout_width="28dp" android:layout_height="28dp"> </ImageView> </RelativeLayout> <com.example.kotlindemo.widget.weixin.WeiXinChatSessionListView android:paddingLeft="15dp" android:paddingRight="15dp" android:dividerHeight="10dp" android:id="@+id/chat_list" android:background="#FBFAFA" android:layout_width="match_parent" android:layout_height="match_parent"> </com.example.kotlindemo.widget.weixin.WeiXinChatSessionListView> </com.example.kotlindemo.widget.weixin.WeiXinMainLayout> </com.example.kotlindemo.widget.weixin.WeiXinMainPullViewGroup> <LinearLayout android:background="@color/navigation_color" android:orientation="vertical" android:id="@+id/navigation" android:layout_alignParentBottom="true" android:layout_width="match_parent" android:layout_height="60dp"> </LinearLayout> <View android:background="@color/navigation_color" android:id="@+id/status_bar" android:layout_width="match_parent" android:layout_height="100dp"></View> </RelativeLayout> </layout>
以上就是Android 仿微信小程序入口動畫的詳細內(nèi)容,更多關(guān)于Android 微信小程序入口動畫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中@id和@+id及@android:id的區(qū)別介紹
這篇文章主要給大家介紹了關(guān)于Android中@id和@+id及@android:id的區(qū)別的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09Android 連接Wifi和創(chuàng)建Wifi熱點的實例
本篇文章介紹了Android 連接Wifi和創(chuàng)建Wifi熱點,小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧。2016-10-10android系統(tǒng)按鍵音framework流程源碼詳細解析
這篇文章主要為大家詳細介紹了android系統(tǒng)按鍵音framework流程源碼,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08準確測量 Android 應(yīng)用中 Activity 和 Fragmen
在 Android 應(yīng)用開發(fā)中,了解每個 Activity 和 Fragment 的啟動時間對于性能優(yōu)化至關(guān)重要,本文將介紹幾種方法來準確測量 Activity 和 Fragment 的啟動時間,并提供實際操作步驟,以幫助提升應(yīng)用的響應(yīng)速度和用戶體驗,需要的朋友可以參考下2024-07-07Android實現(xiàn)雙模(CDMA/GSM)手機短信監(jiān)聽的方法
這篇文章主要介紹了Android實現(xiàn)雙模(CDMA/GSM)手機短信監(jiān)聽的方法,涉及Android短信的原理與相關(guān)操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下2016-06-06Android實現(xiàn)滑動刪除操作(PopupWindow)
這篇文章主要介紹了Android ListView結(jié)合PopupWindow實現(xiàn)滑動刪除的相關(guān)資料,需要的朋友可以參考下2016-07-07Android數(shù)據(jù)加密之Rsa加密的簡單實現(xiàn)
下面小編就為大家?guī)硪黄狝ndroid數(shù)據(jù)加密之Rsa加密的簡單實現(xiàn)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10