Android仿考拉全局滑动返回及联动效果的实现方法

前言

首次通过右滑来返回到上一个页面的操作是在 IOS7上出现。到目前android应用上支持这种操作的依然不多。分析其主要原因应该是android已有实体的返回按键,这样的功能变得不重要,但我觉得有这样的功能便于单手操作,能提升app的用户体验,特别是从ios转到android的用户。写这篇博文希望可以对大家有所帮助,希望自己的app上有滑动返回功能的可以参考下。

原理的简单描述

Android系统里有很多滑动相关的API和类,比如ViewDragHelper就是一个很好的滑动助手类。首先设置Window的背景为透明,再通过ViewDragHelper对Activity上DecorView的子view进行滑动,当滑动到一定距离,手指离开后就自动滑到最右侧,然后finish当前的activity,这样即可实现滑动返回效果。为了能够 “全局的”、“联动的” 实现滑动返回效果,在每个activity的DecorView下插入了SwipeBackLayout,当前activity滑动和下层activity的联动都在该类中完成。

效果图

布局图

实现主要类:

SwipeBackActivity //滑动返回基类

SwipeBackLayout //滑动返回布局类

SwipeBackLayoutDragHelper  //修改ViewDragHelper后助手类

TranslucentHelper //代码中修改透明或者不透明的助手类

##代码层面的讲解

一. 设置activity为透明、activity跳转动画(TranslucentHelper 讲解)

这个看起来很简单,但如果要兼容到API16及以下,会遇到过一个比较麻烦的页面切换动画问题:

1.1、通过activity的主题style进行设置

<item name="android:windowBackground">@color/transparent</item>
<item name="android:windowIsTranslucent">true</item>```

**遇到问题:**如果在某个activity的主题style中设置了android:windowIsTranslucent属性为true,那么该activity切换动画与没设置之前是不同的,有些手机切换动画会变得非常跳。所以需要自定义activity的切换动画。
接下来我们会想到通过主题style里的windowAnimationStyle来设置切换动画

@anim/activity_open_enter

@anim/activity_open_exit

@anim/activity_close_enter

@anim/activity_close_exit```

**实践证明:**当android:windowIsTranslucent为true时,以上几个属性是无效的,而下面两个属性还是可以用。但是这两个属性一个是窗口进来动画,一个是窗口退出动画,明显是不够。

<item name="android:windowEnterAnimation">@anim/***</item>
<item name="android:windowExitAnimation">@anim/***</item>``

结合overridePendingTransition(int enterAnim, int exitAnim)可以复写窗口进来动画和窗口退出动画,这种我觉得最终可能是可以实现的,不过控制起来比较复杂:

比如有A、B、C三个页面:

A跳到B,进场页面B动画从右进来,出场页面A动画从左出去,可以直接在style中写死

@anim/***

@anim/***```

如果B返回到A,进场页面A动画从左进来,出场页面B动画从右出去,此时需要通过复写onBackPressed() 方法,
在其中添加overridePendingTransition(int enterAnim, int exitAnim)方法来改变动画。

如果B是finish()后到A页面,在finish()后面加上overridePendingTransition ……

由于onBackPressed() 方法最终会调finish(),所以实际上只需要复写finish(),在其中添加overridePendingTransition……

但是假如B finish()后跳到C,则又不应该执行overridePendingTransition……,那么就需要判断finish执行后是否要加 overridePendingTransition……

对于一个较为庞大的项目,采取这种方法需要对每个页面进行排查,因此是不可行的,而对于刚刚起步的应用来说则是一个选择。

1.2、通过透明助手类(TranslucentHelper)进行设置

透明助手类(TranslucentHelper)里主要又有两个方法,一个是让activity变不透明,一个是让activity变透明,这两个都是通过反射来调用隐藏的系统api来实现的。因为较低的版本不支持代码中修改背景透明不透明,所以在类中有个静态变量mTranslucentState 来记录是否可以切换背景,这样低版本就不需要每次都反射通过捕获到的异常来做兼容方案。

另外:发现有些手机支持背景变黑,但不支持背景变透明(中兴z9 mini 5.0.2系统)

public class TranslucentHelper {
 private static final String TRANSLUCENT_STATE = "translucentState";
 private static final int INIT = 0;//表示初始
 private static final int CHANGE_STATE_FAIL = INIT + 1;//表示确认不可以切换透明状态
 private static final int CHANGE_STATE_SUCCEED = CHANGE_STATE_FAIL + 1;//表示确认可以切换透明状态
 private static int mTranslucentState = INIT;

 interface TranslucentListener {
  void onTranslucent();
 }

 private static class MyInvocationHandler implements InvocationHandler {
  private TranslucentListener listener;

  MyInvocationHandler(TranslucentListener listener) {
   this.listener = listener;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   try {
    boolean success = (boolean) args[0];
    if (success && listener != null) {
     listener.onTranslucent();
    }
   } catch (Exception ignored) {
   }
   return null;
  }
 }

 static boolean convertActivityFromTranslucent(Activity activity) {
  if (mTranslucentState == INIT) {
   mTranslucentState = PreferencesUtils.getInt(TRANSLUCENT_STATE, INIT);
  }
  if (mTranslucentState == INIT) {
   convertActivityToTranslucent(activity, null);
  } else if (mTranslucentState == CHANGE_STATE_FAIL) {
   return false;
  }

  try {
   Method method = Activity.class.getDeclaredMethod("convertFromTranslucent");
   method.setAccessible(true);
   method.invoke(activity);
   mTranslucentState = CHANGE_STATE_SUCCEED;
   return true;
  } catch (Throwable t) {
   mTranslucentState = CHANGE_STATE_FAIL;
   PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
   return false;
  }
 }

 static void convertActivityToTranslucent(Activity activity, final TranslucentListener listener) {
  if (mTranslucentState == CHANGE_STATE_FAIL) {
   if (listener != null) {
    listener.onTranslucent();
   }
   return;
  }

  try {
   Class<?>[] classes = Activity.class.getDeclaredClasses();
   Class<?> translucentConversionListenerClazz = null;
   for (Class clazz : classes) {
    if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
     translucentConversionListenerClazz = clazz;
    }
   }

   MyInvocationHandler myInvocationHandler = new MyInvocationHandler(listener);
   Object obj = Proxy.newProxyInstance(Activity.class.getClassLoader(),
     new Class[] { translucentConversionListenerClazz }, myInvocationHandler);

   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
    getActivityOptions.setAccessible(true);
    Object options = getActivityOptions.invoke(activity);

    Method method = Activity.class.getDeclaredMethod("convertToTranslucent",
      translucentConversionListenerClazz, ActivityOptions.class);
    method.setAccessible(true);
    method.invoke(activity, obj, options);
   } else {
    Method method =
      Activity.class.getDeclaredMethod("convertToTranslucent", translucentConversionListenerClazz);
    method.setAccessible(true);
    method.invoke(activity, obj);
   }
   mTranslucentState = CHANGE_STATE_SUCCEED;
  } catch (Throwable t) {
   mTranslucentState = CHANGE_STATE_FAIL;
   PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
   new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
     if (listener != null) {
      listener.onTranslucent();
     }
    }
   }, 100);
  }
 }
}

让activity变不透明的方法比较简单;让activity变透明的方法参数里传入了一个listener接口 ,主要是当antivity变透明后会回调,因为这个接口也在activity里,而且是私有的,所以我们只能通过动态代理去获取这个回调。最后如果版本大于等于5.0,还需要再传入一个ActivityOptions参数。

在实际开发中,这两个方法在android 5.0以上是有效的,在5.0以下需要当android:windowIsTranslucent为true时才有效,这样又回到了之前的问题activity切换动画异常。

**最终决解方法:**setContentView之前就调用 convertActivityFromTranslucent方法,让activity背景变黑,这样activity切换效果就正常。

**总结:**在style中设置android:windowIsTranslucent为true ,setContentView之前就调用 convertActivityFromTranslucent方法,当触发右滑时调用convertActivityToTranslucent,通过动态代理获取activity变透明后的回调,在回调后允许开始滑动。

二. 让BaseActivity继承SwipeBackActivity(SwipeBackActivity讲解)

先直接看代码,比较少

public abstract class SwipeBackActivity extends CoreBaseActivity {
 /**
  * 滑动返回View
  */
 private SwipeBackLayout mSwipeBackLayout;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  if (!isSwipeBackDisableForever()) {
   TranslucentHelper.convertActivityFromTranslucent(this);
   mSwipeBackLayout = new SwipeBackLayout(this);
  }
 }

 @Override
 protected void onPostCreate(Bundle savedInstanceState) {
  super.onPostCreate(savedInstanceState);
  if (!isSwipeBackDisableForever()) {
   mSwipeBackLayout.attachToActivity(this);
   mSwipeBackLayout.setOnSwipeBackListener(new SwipeBackLayout.onSwipeBackListener() {
    @Override
    public void onStart() {
     onSwipeBackStart();
    }

    @Override
    public void onEnd() {
     onSwipeBackEnd();
    }
   });
  }
 }

 @Override
 public void onWindowFocusChanged(boolean hasFocus) {
  super.onWindowFocusChanged(hasFocus);
  if (!isSwipeBackDisableForever() && hasFocus) {
   getSwipeBackLayout().recovery();
  }
 }

 /**
  * 滑动返回开始时的回调
  */
 protected void onSwipeBackStart() {}

 /**
  * 滑动返回结束时的回调
  */
 protected void onSwipeBackEnd() {}

 /**
  * 设置是否可以边缘滑动返回,需要在onCreate方法调用
  */
 public void setSwipeBackEnable(boolean enable) {
  if (mSwipeBackLayout != null) {
   mSwipeBackLayout.setSwipeBackEnable(enable);
  }
 }

 public boolean isSwipeBackDisableForever() {
  return false;
 }

 public SwipeBackLayout getSwipeBackLayout() {
  return mSwipeBackLayout;
 }
}

SwipeBackActivity中包含了一个SwipeBackLayout对象

在onCreate方法中:

1.activity转化为不透明。

2.new了一个SwipeBackLayout。

在onPostCreate方法中:

1.attachToActivity主要是插入SwipeBackLayout、窗口背景设置……

2.设置了滑动返回开始和结束的监听接口,建议在滑动返回开始时,把PopupWindow给dismiss掉。

onWindowFocusChanged 方法中

如果是hasFocus == true,就recovery()这个SwipeBackLayout,这个也是因为下层activity有联动效果而移动了SwipeBackLayout,所以需要recovery()下,防止异常情况。

isSwipeBackDisableForever 方法是一个大开关,默认返回false,在代码中复写后返回 true,则相当于直接继承了SwipeBackActivity的父类。

setSwipeBackEnable 方法是一个小开关,设置了false之后就暂时不能滑动返回了,可以在特定的时机设置为true,就恢复滑动返回的功能。

**总结说明:**下层activity设置了setSwipeBackEnable 为 false,上层activity滑动时还是可以联动的,比如MainActivity。而isSwipeBackDisableForever 返回true就不会联动了,而且一些仿PopupWindow的activity需要复写这个方法,因为activity需要透明。

三、滑动助手类的使用和滑动返回布局类的实现(SwipeBackLayout讲解)

直接贴SwipeBackLayout源码:

/**
 * 滑动返回容器类
 */
public class SwipeBackLayout extends FrameLayout {
 /**
  * 滑动销毁距离比例界限,滑动部分的比例超过这个就销毁
  */
 private static final float DEFAULT_SCROLL_THRESHOLD = 0.5f;
 /**
  * 滑动销毁速度界限,超过这个速度就销毁
  */
 private static final float DEFAULT_VELOCITY_THRESHOLD = ScreenUtils.dpToPx(250);
 /**
  * 最小滑动速度
  */
 private static final int MIN_FLING_VELOCITY = ScreenUtils.dpToPx(200);
 /**
  * 左边移动的像素值
  */
 private int mContentLeft;
 /**
  * 左边移动的像素值 / (ContentView的宽+阴影)
  */
 private float mScrollPercent;
 /**
  * (ContentView可见部分+阴影)的比例 (即1 - mScrollPercent)
  */
 private float mContentPercent;
 /**
  * 阴影图
  */
 private Drawable mShadowDrawable;
 /**
  * 阴影图的宽
  */
 private int mShadowWidth;
 /**
  * 内容view,DecorView的原第一个子view
  */
 private View mContentView;
 /**
  * 用于记录ContentView所在的矩形
  */
 private Rect mContentViewRect = new Rect();
 /**
  * 设置是否可滑动
  */
 private boolean mIsSwipeBackEnable = true;
 /**
  * 是否正在放置
  */
 private boolean mIsLayout = true;
 /**
  * 判断背景Activity是否启动进入动画
  */
 private boolean mIsEnterAnimRunning = false;
 /**
  * 是否是透明的
  */
 private boolean mIsActivityTranslucent = false;
 /**
  * 进入动画(只在释放手指时使用)
  */
 private ObjectAnimator mEnterAnim;
 /**
  * 退拽助手类
  */
 private SwipeBackLayoutDragHelper mViewDragHelper;
 /**
  * 执行滑动时的最顶层Activity
  */
 private Activity mTopActivity;
 /**
  * 后面的Activity的弱引用
  */
 private WeakReference<Activity> mBackActivityWeakRf;
 /**
  * 监听滑动开始和结束
  */
 private onSwipeBackListener mListener;

 public SwipeBackLayout(Context context) {
  super(context);
  init(context);
 }

 public SwipeBackLayout(Context context, AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }

 public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init(context);
 }

 private void init(Context context) {
  mViewDragHelper = SwipeBackLayoutDragHelper.create(SwipeBackLayout.this, new ViewDragCallback());
  mViewDragHelper.setEdgeTrackingEnabled(SwipeBackLayoutDragHelper.EDGE_LEFT);
  mViewDragHelper.setMinVelocity(MIN_FLING_VELOCITY);
  mViewDragHelper.setMaxVelocity(MIN_FLING_VELOCITY * 2);
  try {
   mShadowDrawable = context.getResources().getDrawable(R.drawable.swipeback_shadow_left);
  } catch (Exception ignored) {
  }
 }

 @Override
 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  try {
   if (!mIsSwipeBackEnable) {
    super.onLayout(changed, left, top, right, bottom);
    return;
   }
   mIsLayout = true;
   if (mContentView != null) {
    mContentView.layout(mContentLeft, top, mContentLeft + mContentView.getMeasuredWidth(),
      mContentView.getMeasuredHeight());
   }
   mIsLayout = false;
  } catch (Exception e) {
   super.onLayout(changed, left, top, right, bottom);
  }
 }

 @Override
 public void requestLayout() {
  if (!mIsLayout || !mIsSwipeBackEnable) {
   super.requestLayout();
  }
 }

 @Override
 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
  try {
   //绘制阴影
   if (mContentPercent > 0
     && mShadowDrawable != null
     && child == mContentView
     && mViewDragHelper.getViewDragState() != SwipeBackLayoutDragHelper.STATE_IDLE) {
    child.getHitRect(mContentViewRect);
    mShadowWidth = mShadowDrawable.getIntrinsicWidth();
    mShadowDrawable.setBounds(mContentViewRect.left - mShadowWidth, mContentViewRect.top,
      mContentViewRect.left, mContentViewRect.bottom);
    mShadowDrawable.draw(canvas);
   }
   return super.drawChild(canvas, child, drawingTime);
  } catch (Exception e) {
   return super.drawChild(canvas, child, drawingTime);
  }
 }

 @Override
 public void computeScroll() {
  mContentPercent = 1 - mScrollPercent;
  if (mViewDragHelper.continueSettling(true)) {
   ViewCompat.postInvalidateOnAnimation(this);
  }
 }

 @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
  if (!mIsSwipeBackEnable) {
   return false;
  }
  try {
   return mViewDragHelper.shouldInterceptTouchEvent(event);
  } catch (ArrayIndexOutOfBoundsException e) {
   return super.onInterceptTouchEvent(event);
  }
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  if (!mIsSwipeBackEnable) {
   return false;
  }
  try {
   mViewDragHelper.processTouchEvent(event);
   return true;
  } catch (Exception e) {
   return super.onTouchEvent(event);
  }
 }

 /**
  * 将View添加到Activity
  */
 public void attachToActivity(Activity activity) {
  //插入SwipeBackLayout
  ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
  ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
  decor.removeView(decorChild);
  if (getParent() != null) {
   decor.removeView(this);
  }
  decor.addView(this);
  this.removeAllViews();
  this.addView(decorChild);

  //mContentView为SwipeBackLayout的直接子view,获取window背景色进行赋值
  activity.getWindow().setBackgroundDrawableResource(R.color.transparent_white);
  TypedArray a = activity.getTheme().obtainStyledAttributes(new int[] {
    android.R.attr.windowBackground
  });
  mContentView = decorChild;
  mContentView.setBackgroundResource(a.getResourceId(0, 0));
  a.recycle();

  //拿到顶层activity和下层activity,做联动操作
  mTopActivity = activity;
  Activity backActivity = ActivityUtils.getSecondTopActivity();
  if (backActivity != null && backActivity instanceof SwipeBackActivity) {
   if (!((SwipeBackActivity) backActivity).isSwipeBackDisableForever()) {
    mBackActivityWeakRf = new WeakReference<>(backActivity);
   }
  }
 }

 /**
  * 设置是否可以滑动返回
  */
 public void setSwipeBackEnable(boolean enable) {
  mIsSwipeBackEnable = enable;
 }

 public boolean isActivityTranslucent() {
  return mIsActivityTranslucent;
 }

 /**
  * 启动进入动画
  */
 private void startEnterAnim() {
  if (mContentView != null) {
   ObjectAnimator anim =
     ObjectAnimator.ofFloat(mContentView, "TranslationX", mContentView.getTranslationX(), 0f);
   anim.setDuration((long) (125 * mContentPercent));
   mEnterAnim = anim;
   mEnterAnim.start();
  }
 }

 protected View getContentView() {
  return mContentView;
 }

 private class ViewDragCallback extends SwipeBackLayoutDragHelper.Callback {
  @Override
  public boolean tryCaptureView(View child, int pointerId) {
   if (mIsSwipeBackEnable && mViewDragHelper.isEdgeTouched(SwipeBackLayoutDragHelper.EDGE_LEFT, pointerId)) {
    TranslucentHelper.convertActivityToTranslucent(mTopActivity,
      new TranslucentHelper.TranslucentListener() {
       @Override
       public void onTranslucent() {
        if (mListener != null) {
         mListener.onStart();
        }
        mIsActivityTranslucent = true;
       }
      });
    return true;
   }
   return false;
  }

  @Override
  public int getViewHorizontalDragRange(View child) {
   return mIsSwipeBackEnable ? SwipeBackLayoutDragHelper.EDGE_LEFT : 0;
  }

  @Override
  public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
   super.onViewPositionChanged(changedView, left, top, dx, dy);
   if (changedView == mContentView) {
    mScrollPercent = Math.abs((float) left / mContentView.getWidth());
    mContentLeft = left;
    //未执行动画就平移
    if (!mIsEnterAnimRunning) {
     moveBackActivity();
    }
    invalidate();
    if (mScrollPercent >= 1 && !mTopActivity.isFinishing()) {
     if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
      ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().invalidate();
     }
     mTopActivity.finish();
     mTopActivity.overridePendingTransition(0, 0);
    }
   }
  }

  @Override
  public void onViewReleased(View releasedChild, float xvel, float yvel) {
   if (xvel > DEFAULT_VELOCITY_THRESHOLD || mScrollPercent > DEFAULT_SCROLL_THRESHOLD) {
    if (mIsActivityTranslucent) {
     mViewDragHelper.settleCapturedViewAt(releasedChild.getWidth() + mShadowWidth, 0);
     if (mContentPercent < 0.85f) {
      startAnimOfBackActivity();
     }
    }
   } else {
    mViewDragHelper.settleCapturedViewAt(0, 0);
   }
   if (mListener != null) {
    mListener.onEnd();
   }
   invalidate();
  }

  @Override
  public int clampViewPositionHorizontal(View child, int left, int dx) {
   return Math.min(child.getWidth(), Math.max(left, 0));
  }

  @Override
  public void onViewDragStateChanged(int state) {
   super.onViewDragStateChanged(state);

   if (state == SwipeBackLayoutDragHelper.STATE_IDLE && mScrollPercent < 1f) {
    TranslucentHelper.convertActivityFromTranslucent(mTopActivity);
    mIsActivityTranslucent = false;
   }
  }

  @Override
  public boolean isTranslucent() {
   return SwipeBackLayout.this.isActivityTranslucent();
  }
 }

 /**
  * 背景Activity开始进入动画
  */
 private void startAnimOfBackActivity() {
  if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
   mIsEnterAnimRunning = true;
   SwipeBackLayout swipeBackLayout = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout();
   swipeBackLayout.startEnterAnim();
  }
 }

 /**
  * 移动背景Activity
  */
 private void moveBackActivity() {
  if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
   View view = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().getContentView();
   if (view != null) {
    int width = view.getWidth();
    view.setTranslationX(-width * 0.3f * Math.max(0f, mContentPercent - 0.15f));
   }
  }
 }

 /**
  * 回复界面的平移到初始位置
  */
 public void recovery() {
  if (mEnterAnim != null && mEnterAnim.isRunning()) {
   mEnterAnim.end();
  } else {
   mContentView.setTranslationX(0);
  }
 }

 interface onSwipeBackListener {
  void onStart();

  void onEnd();
 }

 public void setOnSwipeBackListener(onSwipeBackListener listener) {
  mListener = listener;
 }
}

attachToActivity

上面讲到SwipeBackLayout是在activity的onCreate时被创建,在onPostCreate是插入到DecorView里,主要是因为DecorView是在setContentView时与Window关联起来插入SwipeBackLayout方法如代码所示,不难,然后是设置了window的背景色为透明色,mContentView为SwipeBackLayout的直接子view,获取window背景色进行赋值。由于考拉项目已经有很多activity,而这些activity中android:windowBackground设置的颜色大部分是白色,少部分是灰色和透明的,所以需要在代码中设置统一设置一遍透明的,原来的背景色则赋值给SwipeBackLayout的子View就可以达到最少修改代码的目的。最后拿到顶层activity和下层activity,做联动操作

SwipeBackLayoutDragHelper 和 ViewDragCallback

SwipeBackLayout中包含了一个滑动助手类(SwipeBackLayoutDragHelper)的对象,该类是在ViewDragHelper的基础上进行修改得来的。

修改点:

1.右侧触发区域 EDGE_SIZE由 20dp 改到 25dp

2.提供滑动最大速度 的设置方法

3.在ViewDragHelper 的内部类Callback方法中提供是否activity为透明的回调接口

4.在最终调用滑动的方法dragTo中添加判断逻辑,activity为透明时才支持滑动

SwipeBackLayoutDragHelper 在init 方法中初始化,通过onInterceptTouchEvent和onTouchEvent拿到滑动事件,通过ViewDragCallback的一些方法返回相应的滑动回调,ViewDragCallback实现了SwipeBackLayoutDragHelper.Callback里的以下几个接口,其中其中isTranslucent()是自己添加进去的。

tryCaptureView方法当触摸到SwipeBackLayout里的子View时触发的,当返回true,表示捕捉成功,否则失败。判断条件是如果支持滑动返回并且是左侧边距被触摸时才可以,我们知道这个时候的的背景色是不透明的,如果直接开始滑动则是黑色的,所以需要在这里背景色改成透明的,如果直接调用 TranslucentHelper.convertActivityToTranslucent(mTopActivity, null)后直接返回true,会出现一个异常情况,就是滑动过快时会导致背景还来不及变成黑色就滑动出来了,之后才变成透明的,从而导致了会从黑色到透明的一个闪烁现象,解决的办法是在代码中用了一个回调和标记,当变成透明后设置了mIsActivityTranslucent = true;通过mIsActivityTranslucent 这个变量来判断是否进行移动的操作。由于修改activity变透明的方法是通过反射的,不能简单的设置一个接口后进行回调,而是通过动态代理的方式来实现的(InvocationHandler),在convertToTranslucent方法的第一个参数刚好是一个判断activity是否已经变成透明的回调,看下面代码中 if 语句里的注释和回调,如果窗口已经变成透明的话,就传了一个drawComplete (true)。通过动态代理,将translucentConversionListenerClazz 执行其方法onTranslucentConversionComplete的替换成myInvocationHandler中执行invoke方法。其中赋值给success的args[0]正是 drawComplete。

isTranslucent是自己添加了一个方法,主要是返回activity是否是透明的默认为true,在SwipeBackLayout重写后将mIsActivityTranslucent返回。仔细看SwipeBackLayoutDragHelper方法的话,会发现最后通过dragTo方法对view进行移动,因此在进行水平移动前判断下是否是透明的,只有透明了才能移动

onViewPositionChanged view移动过程中会持续调用,这里面的逻辑主要有这几个:

1.实时计算滑动了多少距离,用于绘制左侧阴影等

2.使下面的activity进行移动moveBackActivity();

3.当view完全移出屏幕后,销毁当前的activity

onViewReleased是手指释放后触发的一个方法。如果滑动速度大于最大速度或者滑动的距离大于设定的阈值距离,则直接移到屏幕外,同时触发下层activity的复位动画,否则移会到原来位置 。

onViewDragStateChanged当滑动的状态发生改变时的回调,主要是停止滑动后,将背景改成不透明,这样跳到别的页面是动画就是正常的。

clampViewPositionHorizontal 返回水平移动距离,防止滑出父 view。

getViewHorizontalDragRange对于clickable=true的子view,需要返回大于0的数字才能正常捕获。

其他方法都较为简单,注释也写了,就不多说了,最后毫不吝啬的贴上SwipeBackLayoutDragHelper的dragTo代码,就多了if (mCallback.isTranslucent())

private void dragTo(int left, int top, int dx, int dy) {
    int clampedX = left;
    int clampedY = top;
    final int oldLeft = mCapturedView.getLeft();
    final int oldTop = mCapturedView.getTop();
    if (dx != 0) {
      clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
      if (mCallback.isTranslucent()) {
        ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
      }
    }
    if (dy != 0) {
      clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
      ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
    }

    if (dx != 0 || dy != 0) {
      final int clampedDx = clampedX - oldLeft;
      final int clampedDy = clampedY - oldTop;
      if (mCallback.isTranslucent()) {
        mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
            clampedDx, clampedDy);
      }
    }
  }

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Android双击返回键退出程序的实现方法

    本文实例讲述了Android双击返回键退出程序的实现方法,是Android程序开发中一个非常实用的功能,分享给大家供大家参考之用.具体方法如下: 一.实现思路: 用户按下返回键时设定一个定时器来监控是否2秒内实现了退出,如果用户没有接着按返回键,则清除第一次按返回键的效果,使程序还原到第一次按下返回键之前的状态.定时器是每次用户按下返回键才去创建. 二.功能代码: /** * 菜单.返回键响应 */ @Override public boolean onKeyDown(int keyCode,

  • Android开发笔记之:返回键的复写onBackPressed()介绍

    在android开发中,当不满足触发条件就按返回键的时候,就要对此进行检测.尤其是当前Activity需要往前一个Activity传送消息时.即Activity1跳转到Activity3如果采用的是startActivityForResult这种方式,如果不重写返回键,程序不知道要返回给Activity1什么内容就会报错.因此,必须对Activity3的返回按键重写,这里让他传一个"ERROR"信息: 复制代码 代码如下: @Override    public void onBack

  • Android中让按钮拥有返回键功能的方法及重写返回键功能

    让按钮拥有返回键的功能很简单,在点击事件加上finish();就OK了. 如: 复制代码 代码如下: public void onClick(View v){ finish(); } finish() 仅仅是把activity从当前的状态退出,但是资源并没有给清理. 其实android的机制决定了用户无法完全退出application,即使用System.exit(). android自己决定何时该从内存中释放程序,当系统没有可用内存时,就会按照一定的优先级来销毁应用程序. android手机操

  • Android 滑动返回Activity的实现代码

    Android 滑动返回Activity的实现代码 近来玩微信的时候偶然发现,向左滑动朋友圈竟然可以返回主页,故引起兴趣特研究 代码很简洁 package com.example.wyj.cainiaoshopping.activity; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import com.

  • android监听返回按钮事件的方法

    本文实例讲述了android监听返回按钮事件的方法.分享给大家供大家参考.具体如下: 用户在点击手机的返回按钮时,默认是推出当前的activty,但是有时用户不小心按到返回,所以需要给用户一个提示,这就需要重写onkeydown事件,实现的效果如下: java代码如下: @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.l

  • Android返回键功能的实现方法

    本文实例讲述了Android返回键功能的实现方法.分享给大家供大家参考.具体如下: 在开发android应用时,常常通过按返回键(即keyCode == KeyEvent.KEYCODE_BACK)就能关闭程序,其实大多情况下并没有关闭改应用 我们可以这样做,当用户点击自定义的退出按钮或返回键时(需要捕获动作),我们在onDestroy()里强制退出应用,或直接杀死进程,具体操作代码如下: public boolean onKeyDown(int keyCode, KeyEvent event)

  • Android使用SlidingPaneLayout 实现仿微信的滑动返回

    上周,公司的项目改版要求加上一个右滑返回上一个界面,于是就在网上找了一些开源库打算实现.但是在使用的时候遇见了许多的问题.试了两天用过 https://github.com/ikew0ng/SwipeBackLayout , https://github.com/r0adkll/Slidr 等库都没成功. 然后在//www.jb51.net/article/138869.htm看见了使用SlidingPaneLayout 来实现的一个滑动返回案例然后就看了看发现不错于是就使用了这个. 虽然上面链

  • Android 再按一次返回键退出程序实现思路

    用户退出应用前给出一个提示是很有必要的,因为可能是用户并不真的想退出,而只是一不小心按下了返回键,大部分应用的做法是在应用退出去前给出一个Dialog,我觉得这样不太友好,用户还得移动手指去按dialog中的按钮.个人觉得"再按一次返回键退出程序"是best practice,实现也很简单,直接上代码: 复制代码 代码如下: private long exitTime = 0; @Override public boolean onKeyDown(int keyCode, KeyEve

  • Android仿考拉全局滑动返回及联动效果的实现方法

    前言 首次通过右滑来返回到上一个页面的操作是在 IOS7上出现.到目前android应用上支持这种操作的依然不多.分析其主要原因应该是android已有实体的返回按键,这样的功能变得不重要,但我觉得有这样的功能便于单手操作,能提升app的用户体验,特别是从ios转到android的用户.写这篇博文希望可以对大家有所帮助,希望自己的app上有滑动返回功能的可以参考下. 原理的简单描述 Android系统里有很多滑动相关的API和类,比如ViewDragHelper就是一个很好的滑动助手类.首先设置

  • Android仿微信底部按钮滑动变色

    Android仿微信底部按钮滑动变色,这里只针对使用Fragment为Tab页的滑动操作,进行简单的变色讲解. 首先说下OnPageChangeListener这个监听 //这个监听有三个方法 public abstract void onPageScrollStateChanged (int state) public abstract void onPageScrolled (int position, float positionOffset, int positionOffsetPixe

  • Android仿淘宝view滑动至屏幕顶部会一直停留在顶部的位置

    在刚刚完成的项目中,在一个页面中,用户体验师提出引用户操作的入住按钮要一直保留在页面当中,不管页面能滚动多长都得停留在页面的可视区域.最终实现效果如下图所示:   如图中的红色框中的view始终会停留在页面中,如果滑动至页面的顶部,会一直保留在顶部. 下面来说下具体的实现思路: 思路:其实整个页面当中一共有两个视觉效果一样的View,通过滑动的位置来进行View的隐藏和显示来达到这种效果.整个页面的在上下滑动的过程中可以总结为两个状态,状态A(如图1所示),view2在可视区域内时,view1不

  • Android中判断listview是否滑动到顶部和底部的实现方法

    今天实现listview的下拉刷新和上拉加载的时候,遇到了一个问题,*就是说需要根据listview中滑动的位置来进行下拉刷新和上拉加载.* 具体点,只有当我的listview滑动到最顶部的时候,这时候下拉才执行刷新操作:只有当我的listview滑动到最底部的时候,这时候上拉才执行加载操作. 那么怎么判断listview的滑动位置呢?其实还是比较好解决的,说一下我的想法: 顶部的判断,根据listview中的第一个item距离listview顶部的距离是否为0. 底部的判断,根据listvie

  • Android仿英语流利说取词放大控件的实现方法(附demo源码下载)

    本文实例讲述了Android仿英语流利说取词放大控件的实现方法.分享给大家供大家参考,具体如下: 1 取词放大控件 英语流利说是一款非常帮的口语学习app,在app的修炼页面长按屏幕,会弹出一个放大镜,当手指移到某个单词的附近,可以看到该英文单词会被选中,效果如下图所示: 2 代码示例 该控件挺有意思,于是我写了个简单的demo,完整实例代码点击此处本站下载.,程序运行后的效果如下: 3 实现原理 该控件的实现原理比较简单,下面介绍几个比较重要的类 ① WordView 在实习该控件的过程中,我

  • Android仿微信5实现滑动导航条

    本文实例为大家分享了Android 仿微信5滑动导航效果,供大家参考,具体内容如下 ViewPageAdapter.java package com.rong; import java.util.ArrayList; import java.util.List; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.view.View; publi

  • Android实现下拉放大图片松手自动反弹效果

    本文实例为大家分享了Android实现下拉放大图片松手自动反弹的具体代码,供大家参考,具体内容如下 直接看效果: 下面就是代码 HeadZoomScrollView类 import android.animation.ValueAnimator; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; i

  • Android仿优酷视频的悬浮窗播放效果

    之前接了需求要让视频播放时可以像优酷视频那样在悬浮窗里播放,并且悬浮窗和主播放页面之间要实现无缝切换,项目中使用的是自封装的ijkplayer 这个要求就代表不能在悬浮窗中新建视频控件,所以需要在悬浮窗中复用主页面的视频控件,以达到无缝衔接的效果. 主页面对应的视频控件的父view <FrameLayout android:id="@+id/vw_live" android:layout_width="match_parent" android:layout_

  • Android利用ViewPager实现可滑动放大缩小画廊效果

    画廊在很多的App设计中都有,如下图所示: 该例子是我没事的时候写的一个小项目,具体源码地址请访问https://github.com/AlexSmille/YingMi. 该画廊类似封面的效果,滑到中间的图片会慢慢变大,离开的View会慢慢的缩小,同时可设置滑动监听和点击监听. 网上有很多例子都是通过Gallery实现的,而上例的实现是通过ViewPager实现,解决了性能优化的问题,今天特此把它抽出来,封装一下,以便以后的方便使用.最终实现的效果如下: 使用方式 布局中添加该自定义控件 <R

  • Android中TabLayout+ViewPager实现tab和页面联动效果

    TabLayout+ViewPager实现tab和页面联动效果 xml中: <?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" an

随机推荐