美文网首页
登陆界面的动画

登陆界面的动画

作者: 有点健忘 | 来源:发表于2018-06-29 13:32 被阅读22次

    就是如下两张图的效果,输入法没弹出来的时候顶部有张图片,输入法弹出以后将图片滚动上去,同时状态栏显示当前选中的tab

    image.png

    输入法弹出以后


    image.png

    布局如下,就简单弄一下

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/layout_root2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        android:scrollbars="none">
    
        <LinearLayout
            android:id="@+id/layout_root"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <ImageView
                android:id="@+id/iv_cover"
                android:fitsSystemWindows="true"
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:scaleType="centerCrop"
                android:src="@mipmap/ic_launcher" />
    
            <RadioGroup
                android:id="@+id/rg"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_alignBottom="@+id/iv_cover"
                android:layout_marginTop="-40dp"
                android:orientation="horizontal">
    
                <RadioButton
                    android:id="@+id/rb1"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:checked="true"
                    android:gravity="center"
                    android:text="密码登陆"
                    android:textColor="#fff" />
    
                <RadioButton
                    android:id="@+id/rb2"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:text="验证码登陆"
                    android:textColor="#fff" />
            </RadioGroup>
    
            <LinearLayout
                android:id="@+id/layout_login"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_below="@+id/iv_cover"
                android:orientation="vertical"
                android:padding="10dp">
    
                <EditText
                    android:id="@+id/et_phone"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="10dp"
                    android:hint="phone" />
    
                <EditText
                    android:id="@+id/et_psw"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="10dp"
                    android:hint="psw"
                    android:inputType="number" />
    
                <Button
                    android:id="@+id/btn_login"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="50dp"
                    android:layout_marginTop="20dp"
                    android:layout_marginRight="50dp"
                    android:text="login"
                    android:textAllCaps="false" />
    
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="20dp"
                    android:text="new user" />
            </LinearLayout>
        </LinearLayout>
    </ScrollView>
    

    kotlin代码如下,menifest里需要加入 android:windowSoftInputMode="stateHidden|adjustResize"
    简单说明下 ,因为只有 resize才能有布局变化可以监听
    layout_login,因为它的高度是match_parent,输入法弹出来以后这个高度肯定发生变化了。
    给他添加addOnGlobalLayoutListener,

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_login2)
            addObserver()
            //dp261 就是图片的高度300【减去】最后要显示的radiobutton的高度40,加个1是因为像素float转化int有误差,所以多加了个1
        }
    
        private fun addObserver() {
            layout_login.viewTreeObserver.addOnGlobalLayoutListener(listener)
        }
    
        private fun removeObserver() {
            layout_login.viewTreeObserver.removeOnGlobalLayoutListener(listener)
        }
    
        override fun onDestroy() {
            super.onDestroy()
            removeObserver()
        }
    
        var loginHeight = 0
        val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
            @RequiresApi(Build.VERSION_CODES.M)
            override fun onGlobalLayout() {
                if (loginHeight > 0) {
                    if (layout_login.height < loginHeight) {
                        //高度变小说明键盘弹出来了
                        println("===============键盘弹出来了")
                        startUIAnimator(true)
                    } else if (layout_login.height > loginHeight) {
                        //键盘消失了
                        println("===============键盘消失了")
                        startUIAnimator(false)
                    }
                }
                loginHeight = layout_login.height
            }
    
        }
     var lastAnimator:ValueAnimator?=null
        private fun startUIAnimator(softInputShow:Boolean){
            removeObserver()
    lastAnimator?.cancel()
            ValueAnimator.ofFloat(0f,1f)
                    .apply {
    lastAnimator=this;
                        duration = 500
                        addListener(object : AnimatorListenerAdapter() {
                            override fun onAnimationEnd(animation: Animator?) {
                                super.onAnimationEnd(animation)
                                addObserver()
                            }
                        })
                        addUpdateListener {
                            var value = it.animatedValue as Float
                            if(!softInputShow){
                                value=1-value
                            }
                            layout_root.translationY = -dp2px(261) * value
    
                            if(rb1.isChecked){
                                updateState(rb1,rb2,1-value)
                            }else{
                                updateState(rb2,rb1,1-value)
                            }
                        }
                        start()
                    }
        }
        fun dp2px(dp: Int): Float {
            var displayMetrics = DisplayMetrics()
            windowManager.defaultDisplay.getMetrics(displayMetrics)
            return displayMetrics.density * dp
        }
        val  colorBG=ColorDrawable(Color.BLUE) 
        /**
         * @param vCheck 当前选中的那个,用来变色
         * @param vUncheck 没有选中的那个
         * @param factor 要隐藏的那个vuncheck的当前比重*/
        private fun updateState(vCheck:View,vUncheck:View,factor:Float){
            //vcheck 修改背景色
            colorBG.alpha= ((1-factor)*255).toInt()
            vCheck.background=colorBG
            //vuncheck修改显示的比重
            var param = vUncheck.layoutParams as LinearLayout.LayoutParams
            param.weight =factor
            vUncheck.alpha=factor
            vUncheck.layoutParams = param;
            vUncheck.background=ColorDrawable(Color.TRANSPARENT)
        }
    

    想起以前用过一个找工作的app,登陆的时候,是输入法弹出以后,它上边有个大个logo变小了,然后滚动到右上角了,好像还有一些文字之类的。其实做法都一样了,就是给要发生变化的控件加上动画就完事了。

    新的问题修改

    上边的是对于状态栏不透明的没啥问题,可如果设置了状态栏透明,那么就会有问题,比如主题里加入下边的代码

    <item name="android:windowTranslucentStatus" tools:targetApi="kitkat">true</item>
    

    主要就是通过getWindowVisibleDisplayFrame(rect)方法来获取布局是否改变,这个方法获取的是window窗体的可见大小,所以使用这个页面的哪个view来调用这个方法都可以。
    先看下效果


    image.png
    image.png

    因为这时候要处理状态栏,所以在上边的代码做了下修改
    首先布局文件里添加一个view,如下,主要就是到时候在状态栏显示的

    <View
                android:id="@+id/view_state"
                android:background="#00f"
                android:layout_above="@+id/rg"
                android:visibility="gone"
                android:layout_width="match_parent"
                android:layout_height="0dp"/>
            <RadioGroup
                android:id="@+id/rg"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_alignBottom="@+id/iv_cover"
                android:orientation="horizontal">
    

    其次,我们scrollview的滚动距离需要减去状态栏的高度,要不那个radiogroup就跑到状态栏下边去了
    代码如下,新加的代码都有注释

     override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_login2)
            screenHeight=windowManager.defaultDisplay.height
            stateBarHeight=getStatusHeight(this)
            view_state.layoutParams.apply {
                height=stateBarHeight
                view_state.layoutParams=this
            }
            contentView=findViewById<ViewGroup>(android.R.id.content)
            addObserver()
            //dp261 就是图片的高度300【减去】最后要显示的radiobutton的高度40,加个1是因为像素float转化int有误差,所以多加了个1
        }
        var screenHeight=0
        var stateBarHeight=0;
        lateinit var contentView:ViewGroup
        private fun addObserver() {
            contentView.getChildAt(0)?.viewTreeObserver?.addOnGlobalLayoutListener(listener)
        }
    
        private fun removeObserver() {
            contentView.getChildAt(0)?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
        }
    
        override fun onDestroy() {
            super.onDestroy()
            removeObserver()
        }
        var softInputShow=false;//保存键盘的显示状态,防止切换editTextView引起动画异常
        val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                var rect = Rect() //数字键盘和字母键盘高度不一样,如果在两个editTextView之间切换这里也会走的,
                contentView.getWindowVisibleDisplayFrame(rect)
    
                println("onGlobalLayout======${layout_login.height}===t/b=${layout_login.top}/${layout_login.bottom}===" +
                        "${rect.toShortString()}==$screenHeight")
                    if (!softInputShow&&rect.bottom < screenHeight) {
                        //高度变小说明键盘弹出来了
                        println("===============键盘弹出来了")
                        softInputShow=true
    
                        startUIAnimator(true)
                    } else if (softInputShow&&rect.bottom == screenHeight) {
                        //键盘消失了
                        println("===============键盘消失了")
                        softInputShow=false
                        view_state.visibility=View.GONE//隐藏状态栏那个占位的view
                        startUIAnimator(false)
                    }
            }
    
        }
    
        private fun startUIAnimator(softInputShow:Boolean){
            removeObserver()
            ValueAnimator.ofFloat(0f,1f)
                    .apply {
                        duration = 500
                        addListener(object : AnimatorListenerAdapter() {
                            override fun onAnimationEnd(animation: Animator?) {
                                super.onAnimationEnd(animation)
                                addObserver()
                                if(softInputShow){
                                    view_state.visibility=View.VISIBLE//这里显示状态栏的view
                                }
                            }
                        })
                        addUpdateListener {
                            var value = it.animatedValue as Float
                            if(!softInputShow){
                                value=1-value
                            }
                            layout_root.translationY = ( -dp2px(261)+stateBarHeight) * value
                            //滚动距离减去状态栏高度
                            if(rb1.isChecked){
                                updateState(rb1,rb2,1-value)
                            }else{
                                updateState(rb2,rb1,1-value)
                            }
                        }
                        start()
                    }
        }
        fun dp2px(dp: Int): Float {
            var displayMetrics = DisplayMetrics()
            windowManager.defaultDisplay.getMetrics(displayMetrics)
            return displayMetrics.density * dp
        }
        val  colorBG=ColorDrawable(Color.BLUE)
        /**
         * @param vCheck 当前选中的那个,用来变色
         * @param vUncheck 没有选中的那个
         * @param factor 要隐藏的那个vuncheck的当前比重*/
        private fun updateState(vCheck:View,vUncheck:View,factor:Float){
            //vcheck 修改背景色
            colorBG.alpha= ((1-factor)*255).toInt()
            vCheck.background=colorBG
    
            //vuncheck修改显示的比重
            var param = vUncheck.layoutParams as LinearLayout.LayoutParams
            param.weight =factor
            vUncheck.alpha=factor
            vUncheck.layoutParams = param;
            vUncheck.background=ColorDrawable(Color.TRANSPARENT)
        }
    

    ps补充问题

    自己测试机6.0的测试没啥问题,结果发朋友7.0的手机就出问题了。后来打印日志发现了问题
    问题就出现在contentView.getWindowVisibleDisplayFrame(rect)
    rect,在6.0的手机上获取的top是0,可在7.0 8.0的手机上,他的top就是状态栏的高度了,我之前用的rect.getHeight(),结果就出错了。 为了适配高版本,所以改成 rect.bottom了。这样就都一样了

    最后就是封装下,以后就可以直接用了,网上找了个模板,稍微改了下逻辑。
    因为用的screenBottom。这个没研究过带底部虚拟导航栏的显示隐藏会不会出问题,等有这测试机再修改下代码

    import android.app.Activity;
    import android.arch.lifecycle.GenericLifecycleObserver;
    import android.arch.lifecycle.Lifecycle;
    import android.arch.lifecycle.LifecycleOwner;
    import android.content.Context;
    import android.graphics.Rect;
    import android.view.View;
    import android.view.ViewTreeObserver;
    import android.view.inputmethod.InputMethodManager;
    import android.widget.EditText;
    
    public class SoftKeyBoardListener {
        public interface OnSoftKeyBoardChangeListener {
    
            void keyBoardShow(int height);
    
            void keyBoardHide(int height);
    
        }
    
    
        private View rootView;//activity的根视图
    
        private int screenBottom;//纪录根视图的显示高度
    
        private OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener;
        boolean isShow=false;//软键盘是否显示
        private ViewTreeObserver.OnGlobalLayoutListener listener;
        public SoftKeyBoardListener(Activity activity) {
    
    //获取activity的根视图
            rootView = activity.getWindow().getDecorView();
            screenBottom=activity.getWindowManager().getDefaultDisplay().getHeight();
    //监听视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变
            listener=new ViewTreeObserver.OnGlobalLayoutListener() {
    
                @Override
    
                public void onGlobalLayout() {
    
                    //获取当前根视图在屏幕上显示的大小
    
                    Rect r = new Rect();
    
                    rootView.getWindowVisibleDisplayFrame(r);
    
                    //根视图显示高度变小超过300,可以看作软键盘显示了
                    System.out.println("rect============"+r.toShortString()+"==="+screenBottom);
                    if (!isShow&& screenBottom >r.bottom) {
                        isShow=true;
                        if (onSoftKeyBoardChangeListener != null) {
                            onSoftKeyBoardChangeListener.keyBoardShow(screenBottom - r.bottom);
                        }
                        return;
                    }
    
                    //根视图显示高度变大超过300,可以看作软键盘隐藏了
    
                    if (isShow&&r.bottom>= screenBottom) {
                        isShow=false;
                        if (onSoftKeyBoardChangeListener != null) {
    
                            onSoftKeyBoardChangeListener.keyBoardHide(0);
    
                        }
    
                        return;
    
                    }
    
                }
    
            };
            rootView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
            addLifeObServer(activity);
        }
    
        private void setOnSoftKeyBoardChangeListener(OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener) {
    
            this.onSoftKeyBoardChangeListener = onSoftKeyBoardChangeListener;
    
        }
    
        public static void setListener(Activity activity, OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener) {
    
            SoftKeyBoardListener softKeyBoardListener = new SoftKeyBoardListener(activity);
    
            softKeyBoardListener.setOnSoftKeyBoardChangeListener(onSoftKeyBoardChangeListener);
    
        }
    
        public static void closeKeybord(EditText mEditText, Context mContext) {
    
            InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
    
            imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
    
            mEditText.setFocusable(false);
    
        }
    
        public void addLifeObServer(Activity activity){
            if(activity instanceof LifecycleOwner){
                LifecycleOwner owner= (LifecycleOwner) activity;
                owner.getLifecycle().addObserver(new GenericLifecycleObserver() {
                    @Override
                    public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
                        if(event==Lifecycle.Event.ON_DESTROY){
                            if(rootView!=null)
                                rootView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
                        }
                    }
                });
            }
        }
    }
    

    顺道研究下OnGlobalLayoutListener啥时候调用的

    ~1ViewTreeObserver这个类
    如下,里边一堆集合变量,用来存储我们的listener

        private CopyOnWriteArrayList<OnWindowFocusChangeListener> mOnWindowFocusListeners;
        private CopyOnWriteArrayList<OnWindowAttachListener> mOnWindowAttachListeners;
        private CopyOnWriteArrayList<OnGlobalFocusChangeListener> mOnGlobalFocusListeners;
        private CopyOnWriteArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners;
        private CopyOnWriteArrayList<OnEnterAnimationCompleteListener>
                mOnEnterAnimationCompleteListeners;
    
        // Non-recursive listeners use CopyOnWriteArray
        // Any listener invoked from ViewRootImpl.performTraversals() should not be recursive
        private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
        private CopyOnWriteArray<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners;
        private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners;
        private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
        private CopyOnWriteArray<OnWindowShownListener> mOnWindowShownListeners;
    
        // These listeners cannot be mutated during dispatch
        private boolean mInDispatchOnDraw;
        private ArrayList<OnDrawListener> mOnDrawListeners;
        private static boolean sIllegalOnDrawModificationIsFatal;
    
        /** Remains false until #dispatchOnWindowShown() is called. If a listener registers after
         * that the listener will be immediately called. */
        private boolean mWindowShown;
    
        private boolean mAlive = true;
    

    然后下边就是差不多的,一个add,一个remove

     public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener)
    
    public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim)
    

    再然后,就是分派这些listener的事件,如下

        public final void dispatchOnGlobalLayout() {
            // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
            // perform the dispatching. The iterator is a safe guard against listeners that
            // could mutate the list by calling the various add/remove methods. This prevents
            // the array from being modified while we iterate it.
            final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
            if (listeners != null && listeners.size() > 0) {
                CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
                try {
                    int count = access.size();
                    for (int i = 0; i < count; i++) {
                        access.get(i).onGlobalLayout();
                    }
                } finally {
                    listeners.end();
                }
            }
        }
    

    以OnGlobalLayoutListener为例
    我们要做的就是看这个dispatchOnGlobalLayout方法都哪里调用了。。
    我以为是这样,结果在view,viewgroup里搜了下,没有地方调用这个方法,奇怪

    那么我就去看这个viewtreeobserver哪里来的,在view里如下

        public ViewTreeObserver getViewTreeObserver() {
            if (mAttachInfo != null) {
                return mAttachInfo.mTreeObserver;
            }
            if (mFloatingTreeObserver == null) {
                mFloatingTreeObserver = new ViewTreeObserver(mContext);
            }
            return mFloatingTreeObserver;
        }
    
    //下边的代码,可以看到mFloatingTreeObserver也就是被合并到mAttachInfo.mTreeObserver里了
            if (mFloatingTreeObserver != null) {
                info.mTreeObserver.merge(mFloatingTreeObserver);
                mFloatingTreeObserver = null;
            }
    

    我们现在就得分析这个mAttachInfo 里的observer到里哪里用了。
    看下这个attachinfo哪初始化的,在view或者viewgroup里的这个方法,可以看到是外部传进来的

    void dispatchAttachedToWindow(AttachInfo info, int visibility)
    

    至于这个方法哪里调用的,真不知道。
    所以鼠标点击final static class AttachInfo 这个类,看弹出来的都哪里用到这个类了。


    image.png

    ViewRootImpl

    然后我就搜了下我们需要的dispatchOnGlobalLayout,这里有

            if (triggerGlobalLayoutListener) {
                mAttachInfo.mRecomputeGlobalAttributes = false;
                mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
            }
    

    不继续看了,太复杂了。
    看下这个类的描述

    * The top of a view hierarchy, implementing the needed protocol between View
     * and the WindowManager.  This is for the most part an internal implementation
     * detail of {@link WindowManagerGlobal}.
     *
    

    相关文章

      网友评论

          本文标题:登陆界面的动画

          本文链接:https://www.haomeiwen.com/subject/nbooyftx.html