Android仿今日頭條圖片滑動退出效果

逛CSDN的時候,看到幾篇寫仿今日頭條圖片滑動退出效果的文章,閒着無聊便想着也給自己項目加上,實現的思路有很多種,本着就近原則選了一篇與自己思路相近的文章結合自己的實踐總結一下。

下載原文的Demo用了一下,發現了幾點可以優化的地方:

1、圖片縮放上不太好使,放大縮小的同時就給你滑出去了

2、沒有暴露接口給用戶實現更多的透明度變換效果,比如我不僅想要背景透明度在手指移動的時候發生變化,還有文字或者其他內容也跟着發生透明度變化

3、去掉了一些多餘代碼

實現效果

  1. 在上滑或者下滑時,隨着手指的移動,圖片區域跟隨移動,並且activity的背景和頁碼逐漸變的透明
  2. 上下滑動距離不超過設定的臨界值時,會有回彈效果。
  3. 上下滑動超過設置的臨界值時,放開手指,頁面滑動退出消失
  4. 圖片可以正常放大縮小,頁面不跟隨手指上下滑動
  5. 頁面切換淡入淡出效果

實現原理

實現思路有很多種,這種實現思路對我當前項目改動最小,只是在原來的頁面上嵌套了一箇中間層,在這個中間層上做手腳,根據Android的事件分發機制,在中間層判斷當前手指的移動距離和方向選擇是否攔截事件交由自己處理,實現上下滑動退出的效果,並且不影響圖片正常的放大縮小和切換。

中間層容器實現過程

重寫onInterceptTouchEvent()

這個方法是本次效果實現的核心,用於處理不同的手勢操作,處理的好則各種手勢不發生衝突。這裏返回ture意味着判斷當前操作爲要退出頁面,需要將事件攔截交由自己處理,開始跟隨手指的移動發生滑動動畫。返回false則不對事件進行攔截,交由下面控件處理,比如切換圖片、放大縮小。

上面提到的原Demo中對於圖片放大縮小的同時頁面發送滑動退出的情況,在這裏我加了一個多點觸控的判斷,當觸摸屏幕的點大於一個時認爲是對圖片進行放縮或者切換,所以直接返回false。當觸摸點只有一個時,判斷手指在X、Y軸移動的距離更多的是上下滑動還是左右滑動,認定上下滑動時攔截事件進入自己的onTouchEvent()處理動畫,左右滑動則不攔截,交由viewPager進行圖片切換。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //判斷幾指操作,大於1時認爲在對圖片進行放大縮小操作,不攔截事件
        //交由下面控件處理
        if (ev.getPointerCount() > 1) {
            return false;
        } else {
            final int y = (int) ev.getRawY();
            final int x = (int) ev.getRawX();
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    previousX = x;
                    previousY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    int diffY = y - previousY;
                    int diffX = x - previousX;
                    //當Y軸移動距離大於X軸50個單位時攔截事件
                    //進入onTouchEvent開始處理上下滑動退出效果
                    if (Math.abs(diffX) + 50 < Math.abs(diffY)) {
                        return true;
                    }
                    break;
            }
            return false;
        }

    }

重寫onTouchEvent()

事件被攔截後就會走這個方法,這裏主要是根據手指位移對動畫的操作,原Demo中判斷方向的代碼被我去掉了,因爲原代碼裏的direction永遠是UP_DOWN,多餘不如去掉。這裏我增加了一個回調供外部使用,參數是當前的透明度。

@Override
    public boolean onTouchEvent(@NonNull MotionEvent ev) {

        final int y = (int) ev.getRawY();
        final int x = (int) ev.getRawX();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                previousX = x;
                previousY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int diffY = y - previousY;
                //判斷手指向上還是向下移動,關聯手指擡起後的動畫位移方向
                isScrollingUp = diffY <= 0;
                this.setTranslationY(diffY);
                if (mBackground != null) {
                    //透明度跟隨手指的移動距離發生變化
                    int alpha = (int) (255 * Math.abs(diffY * 1f)) / getHeight();
                    mBackground.setAlpha(255 - alpha);
                    //回調給外面做更多操作
                    mScrollListener.onLayoutScrolling(alpha / 255f);
                    LogUtil.i("alpha is " + alpha);
                }
              
                break;
            case MotionEvent.ACTION_UP:
                int height = this.getHeight();
                //滑動距離超過臨界值才執行退出動畫,臨界值爲控件高度1/4
                if (Math.abs(getTranslationY()) > (height / 4)) {
                    //執行退出動畫
                    layoutExitAnim();
                } else {
                    //執行恢復動畫
                    layoutRecoverAnim();
                }
              
        }
        return true;
    }

退出動畫

這個方法也去掉了一些代碼


    public void layoutExitAnim() {
        ObjectAnimator exitAnim;
        //從手指擡起的位置繼續向上或向下的位移動畫
        exitAnim = ObjectAnimator.ofFloat(this, "translationY", getTranslationY(), isScrollingUp ? -getHeight() : getHeight());
        exitAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //動畫結束時將背景置爲完全透明
                if (mBackground != null) {
                    mBackground.setAlpha(0);
                }
                //執行回調,退出頁面
                if (mScrollListener != null) {
                    mScrollListener.onLayoutClosed();
                }

            }
        });
        exitAnim.addUpdateListener(animation -> {
            if (mBackground != null) {
                //根據位移計算設置背景透明度
                int alpha = (int) (255 * Math.abs(getTranslationY() * 1f)) / getHeight();
                mBackground.setAlpha(255 - alpha);
            }
        });
        exitAnim.setDuration(200);
        exitAnim.start();
    }

恢復動畫

加了一個回調供外部調用

private void layoutRecoverAnim() {
        //從手指擡起的地方恢復到原點
        ObjectAnimator recoverAnim = ObjectAnimator.ofFloat(this, "translationY", this.getTranslationY(), 0);
        recoverAnim.setDuration(100);
        recoverAnim.start();
        if (mBackground != null) {
            //將背景置爲完全不透明
            mBackground.setAlpha(255);
            mScrollListener.onLayoutScrollRevocer();
        }
    }

接口回調

public interface LayoutScrollListener {
        //關閉佈局
        void onLayoutClosed();

        //正在滑動
        void onLayoutScrolling(float alpha);

        //滑動結束並且沒有觸發關閉
        void onLayoutScrollRevocer();
    }

使用

佈局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.ImagesActivity">

    <com.zhicun.tieqi.widget.SlideCloseLayout
        android:id="@+id/slide_close_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.luck.picture.lib.widget.PreviewViewPager
            android:id="@+id/img_viewpager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </com.zhicun.tieqi.widget.SlideCloseLayout>

    <TextView
        android:id="@+id/index"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:gravity="center_horizontal"
        android:padding="10dp"
        android:textColor="@color/white"
        tools:text="3/9" />
</RelativeLayout>

Activity

 //設置activity的背景爲黑色
        getWindow().getDecorView().setBackgroundColor(Color.BLACK);
//給控件設置需要漸變的背景。如果沒有設置這個,則背景不會變化
        mSlideCloseLayout.setGradualBackground(getWindow().getDecorView().getBackground());
        //設置監聽,滑動一定距離後讓Activity結束
        mSlideCloseLayout.setLayoutScrollListener(new SlideCloseLayout.LayoutScrollListener() {
            @Override
            public void onLayoutClosed() {
                onBackPressed();
            }

            @Override
            public void onLayoutScrolling(float alpha) {
                mIndex.setAlpha(1 - (alpha * 5f));
            }

            @Override
            public void onLayoutScrollRevocer() {
                mIndex.setAlpha(1);
            }
        });

重寫返回建事件實現淡入淡出切換效果

 @Override
    public void onBackPressed() {
        finish();
        overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
    }

淡入淡出動畫

fade_in

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/decelerate_interpolator">
    <alpha
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromAlpha="0"
        android:toAlpha="1" />
</set>

fade_out

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/decelerate_interpolator">

    <alpha
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromAlpha="1"
        android:toAlpha="0" />
</set>

設置Activity主題

<style name="SlideCloseTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="windowNoTitle">true</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowNoTitle">true</item>
</style>

------------------------------------------------------------------------------------------------------------------------

到此改造就完成啦,效果用起來不錯,本次內容的核心理解就是安卓的事件分發機制,如果對它還不瞭解的,理解了它再來看本片文章就會輕鬆許多了,對於已經理解並熟練掌握的大佬就當複習一遍知識吧~

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章