逛CSDN的时候,看到几篇写仿今日头条图片滑动退出效果的文章,闲着无聊便想着也给自己项目加上,实现的思路有很多种,本着就近原则选了一篇与自己思路相近的文章结合自己的实践总结一下。
下载原文的Demo用了一下,发现了几点可以优化的地方:
1、图片缩放上不太好使,放大缩小的同时就给你滑出去了
2、没有暴露接口给用户实现更多的透明度变换效果,比如我不仅想要背景透明度在手指移动的时候发生变化,还有文字或者其他内容也跟着发生透明度变化
3、去掉了一些多余代码
实现效果
- 在上滑或者下滑时,随着手指的移动,图片区域跟随移动,并且activity的背景和页码逐渐变的透明
- 上下滑动距离不超过设定的临界值时,会有回弹效果。
- 上下滑动超过设置的临界值时,放开手指,页面滑动退出消失
- 图片可以正常放大缩小,页面不跟随手指上下滑动
- 页面切换淡入淡出效果
实现原理
实现思路有很多种,这种实现思路对我当前项目改动最小,只是在原来的页面上嵌套了一个中间层,在这个中间层上做手脚,根据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>
------------------------------------------------------------------------------------------------------------------------
到此改造就完成啦,效果用起来不错,本次内容的核心理解就是安卓的事件分发机制,如果对它还不了解的,理解了它再来看本片文章就会轻松许多了,对于已经理解并熟练掌握的大佬就当复习一遍知识吧~