2016-08-10 更新
添加毛玻璃背景效果,模糊内存优化,详见 快速模糊效果
在iOS
上的chrome
中有侧滑返回上一个页面的功能,感觉蛮好用的,刚好Android
没有自带的侧滑返回效果,如果使用透明的Activity
的话比较浪费性能,所以打算在实现一个简单的DragBackActivity
,拖动的效果模仿iOS
上的Chrome
侧滑返回。
效果图如下:
使用方法: 继承自DragBackActivity
就可以有侧滑返回效果。 如果想要禁用,重写isDisableDrag()
函数返回true
。 定制返回动画的色彩:
1 2 3 4 5 6 mDragLayer.setPositiveColor(...); mDragLayer.setNegativeColor(...); mDragLayer.setCircleColor(...);
粗略解析:
因为需要实现的目的是继承自这个DragBackActivity
就可以实现拖动返回的效果,因为是靠近边缘的侧滑返回,所以要用到手势处理,需要获取到当前Activity
的根视图,手势的处理是要放在视图层处理的。
每个Activity
都有一个id
为android.R.id.content
的根视图,setContentView
所设置的View
就是该根视图的子View
,本项目就是根据这个特性来移动整个Acivity
的。
知道怎么移动Activity
了,接下来就只剩拦截手势和绘制返回动画了。Android
的事件传递是根据整棵视图树来传递的,所以视图越靠近树的根就越先收到触摸事件。所以我需要在android.R.id.content
上或者同级的地方添加自定义的View
,这时候就需要Window
出场了,window
有一个方法是获取到当前窗口的根视图:
1 2 getWindow().getDecorView();
获取到窗口的根视图之后就可以往上面添加自定义视图了,手势拦截处理写在自定义视图中。废话不多说下面来看源码。
###自定义视图 为了可扩展性我把返回动画和手势处理分开来写
一、返回动画的编写(DragBackHintView
) 为了节省内存提高性能,我决定继承自View
使用Canvas
绘图绘制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static final int START_DRAG_BG = 0xff555555 ;ValueAnimator mCircleAnimator; float mCurrentAnimValue = 0.0f ;int mCurrentX = 0 ;int mIconPositiveColor = 0xff999999 ;int mIconNegativeColor = 0xffffffff ;State mState = State.NotShowCircle; GradientDrawable mCircleDrawable; Paint mTextPaint = new Paint(); String mIconId = "\uf2ea" ; GradientDrawable mDarkBg; int ICON_SIZE = 25 ;int CIRCLE_SIZE = 75 ;
大致的变量如上述代码,绘制图标(演示中的箭头)使用的起始是字体,好处就是占用资源少,可以变色,放大不会有失真,占用内存少。圆圈、阴影采用GradientDrawable
绘制,圆圈的展现、消失动画使用ValueAnimator
。不知道上述类的自行谷歌。
手势拦截处理是交给另外一个视图来处理的,所以在此处需要预留一个对外的接口来获知当前用户滑动到哪个位置了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public void onDrag (int diffX) { mCurrentX = diffX; invalidate(); } Override protected void onDraw (Canvas canvas) { canvas.clipRect(0 , 0 , mCurrentX, getHeight()); int alpha = 0x55 * (getWidth() - mCurrentX + getLeft()) / getWidth(); mDarkBg.setAlpha(alpha); mDarkBg.setBounds(getLeft(), getTop(), getLeft() + mCurrentX, getTop() + getHeight()); mDarkBg.draw(canvas); int size = (int ) (CIRCLE_SIZE * mCurrentAnimValue); if (size > 0 ) { int left = (mCurrentX - size) >> 1 ; int top = (getHeight() - size) >> 1 ; mCircleDrawable.setBounds(left, top, left + size, top + size); mCircleDrawable.draw(canvas); } if (mCurrentAnimValue <= 1.0f ) { int oldR = (mIconPositiveColor >> 16 ) & 0xff ; int oldG = (mIconPositiveColor >> 8 ) & 0xff ; int oldB = mIconPositiveColor & 0xff ; int newR = (mIconNegativeColor >> 16 ) & 0xff ; int newG = (mIconNegativeColor >> 8 ) & 0xff ; int newB = mIconNegativeColor & 0xff ; int mixR = (int ) (oldR * (1 - mCurrentAnimValue) + newR * mCurrentAnimValue); int mixG = (int ) (oldG * (1 - mCurrentAnimValue) + newG * mCurrentAnimValue); int mixB = (int ) (oldB * (1 - mCurrentAnimValue) + newB * mCurrentAnimValue); mTextPaint.setColor(Color.argb(0xff , mixR, mixG, mixB)); } else { mTextPaint.setColor(mIconNegativeColor); } float value = mCurrentX * 1.0f / ICON_SIZE; value = value > 1.0f ? 1.0f : value; if (value > 0 ) { Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); float iconWidth = mTextPaint.measureText(mIconId); float iconHeight = fontMetrics.bottom - fontMetrics.top; int startX = (int ) ((mCurrentX - iconWidth) / 2 ); int startY = (int ) ((getHeight() - iconHeight) / 2 - fontMetrics.top); int centerX = (int ) (startX + iconWidth/2 ); int centerY = (int ) ((getHeight() - iconHeight) / 2 + iconHeight / 2 ); mTextPaint.setAlpha((int ) (value * 0xff )); canvas.scale(value, value, centerX, centerY); canvas.rotate(-90 * (1.0f - value), centerX, centerY); canvas.drawText(mIconId, startX, startY, mTextPaint); } }
上述代码就是在用户拖动的时候所经过的逻辑,绘制步骤主要分为三层,先绘制阴影背景,然后是圆圈,最后是图标。需要注意的是以下几点:
圆圈出现和消失的时候,图标会变色,需要根据圆圈消失、出现的动画数值来设置图标颜色数值。
图标是使用字体来绘制的,需要注意在Android
中绘制文字的时候(drawText
)内的参数
图标从无到有或者从有到无,会有一个旋转缩放渐变的动画,此时的旋转和缩放的操作对象是画布
此外还需要几个接口就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public void showCircle () { if (mState != State.NotShowCircle){ return ; } mState = State.ShowCircle; mCircleAnimator.cancel(); mCircleAnimator.setFloatValues(mCurrentAnimValue, 1.0f ); mCircleAnimator.start(); } public void hideCircle () { if (mState != State.ShowCircle){ return ; } mState = State.NotShowCircle; mCircleAnimator.cancel(); mCircleAnimator.setFloatValues(mCurrentAnimValue, 0.0f ); mCircleAnimator.start(); } public void onDragFinished () { mState = State.ExpandCircle; mCircleAnimator.cancel(); float circleSize = (float ) (Math.sqrt(( getWidth()*getWidth()) + (getHeight()*getHeight()) )); mCircleAnimator.setFloatValues( mCurrentAnimValue, circleSize / CIRCLE_SIZE); mCircleAnimator.start(); }
二、手势处理层(EdgeDragLayer
) 关于侧滑返回的动画已经在上面的视图中处理了,在手势处理层中主要需要处理的就是手势,还有就是手指释放的时候,决定是回到最初的状态还是播放返回上一层的动画。
手势的检测 先看手势检测的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Override public boolean onInterceptTouchEvent (MotionEvent event) { if (mDragState == DragState.PlayAnim){ return true ; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { mStartPoint.set((int )event.getX(), (int )event.getY()); mFirstPointId = event.getPointerId(0 ); if (mStartPoint.x < EDGE_WIDTH) { mDragState = DragState.DragStart; } else { mDragState = DragState.DragCancel; } break ; } case MotionEvent.ACTION_MOVE: { if (mDragState == DragState.DragStart){ int diffX = (int )(event.getX() - mStartPoint.x); if (diffX > MIN_DIS) { mDragState = DragState.IsDragging; } } break ; } } return mDragState == DragState.IsDragging; }
在检测到手指ACTION_DOWN
的时候,我先判断是否小于一个给定的数值EDGE_WIDTH
,当然这个数值不是固定的,和机器的dpi
有关。如果条件成立,把状态设为DragState.DragStart
,之后在ACTION_MOVE
的时候再次判断移动的距离是否达到要求,当然MIN_DIS
也不是固定值,同样和手机屏幕像素密度有关。如果移动的距离> MIN_DIS
了,视图层就会拦截所有触摸事件,接下来的事情就交给onTouchEvent
来处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Override public boolean onTouchEvent (MotionEvent event) { if (mDragState == DragState.PlayAnim){ return true ; } if (event.getPointerId(event.getActionIndex()) != mFirstPointId){ return mDragState != DragState.DragCancel; } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { if (mDragState == DragState.DragStart) { int diffX = (int ) (event.getX() - mStartPoint.x); if (diffX > MIN_DIS) { mDragState = DragState.IsDragging; } } else if (mDragState == DragState.IsDragging) { dispatchDragEvent((int ) (event.getX() - mStartPoint.x)); } break ; } case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { if (mDragState == DragState.IsDragging) { int diffX = (int ) (event.getX() - mStartPoint.x); if (needFinished(event)) { playFinishAnim(diffX); mHintView.onDragFinished(); } else { playCancelAnim(diffX); } } break ; } } return mDragState != DragState.DragCancel; }
在这里面我对多点触控优化了体验,只对第一个触控点起效mVelocityTracker
是检测手指的移动速度用的,当用户快速移动的时候,就算没有超过屏幕的一般我也应该要触发返回的事件。其它的看代码应该能懂,代(wo)码(bu)是(xiang)最(zai)好(xie)的(xia)老(qu)师(le)。
接下去就是把这两个View
添加到Activity
中并简单链接一下即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class DragBackActivity extends AppCompatActivity { private FrameLayout mRootContainer; protected EdgeDragLayer mDragLayer; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); if (!isDisableDrag()) { mRootContainer = (FrameLayout) findViewById(android.R.id.content); setupDragView(); } } protected void setupDragView () { if (mDragLayer != null ){ return ; } mDragLayer = new EdgeDragLayer(this ); ((ViewGroup)getWindow().getDecorView()).addView( mDragLayer, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) ); mDragLayer.setOnDragListener(new EdgeDragLayer.DragListener() { @Override public void onDragEvent (int dis) { dragEvent(dis); } @Override public void onCancelDrag () { } @Override public void onDragBackEnd () { customFinish(); } }); } void customFinish () { finish(); overridePendingTransition(R.anim.drag_activity_enter_anim, R.anim.drag_activity_exit_anim); } void dragEvent (int dis) { if (dis < 0 ){ dis = 0 ; } mRootContainer.setX(dis); } protected boolean isDisableDrag () { return false ; } }
源码地址: https://github.com/qgx446738721/DragBackActivity