转载请标明出处:
http://blog.csdn.net/developer_jiangqq/article/details/50043159
本文出自:【江清清的博客】
(一).前言:
【好消息】个人网站已经上线运行,后面博客以及技术干货等精彩文章会同步更新,请大家关注收藏:http://www.lcode.org
这几天正在更新录制实战项目,整体框架是采用仿照QQ5.X侧滑效果的。那么我们一般的做法就是自定义ViewGroup或者采用开源项目MenuDrawer或者Google提供的控件DrawerLayout等方式来实现。这些的控件的很多效果基本上都是采用实现onInterceptTouchEvent和onTouchEvent这两个方法进行实现,而且都是根据要实现的效果做自定义处理例如:多点触控处理,加速度方面检测以及控制等等。一般这样做作于普通开发人员来讲也是需要很强的程序与逻辑开发能力,幸好Android开发框架给我们提供了一个组件ViewDragHelper。上一讲我们已经就ViewGragHelper的基本使用做了相关讲解(点击进入),今天来分析实现QQ5.X侧滑酷炫效果的实例(开源项目地址点击进入)。
具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。
https://github.com/jiangqqlmj/ViewDragHelperTest
FastDev4Android框架项目地址:https://github.com/jiangqqlmj/FastDev4Android
(二).ViewDragHelper的基本使用
前面我们学习ViewDragHelper的基本使用方法,同时也知道了里边的若干个方法的用途,下面我们还是把基本的使用步骤温习一下。要使用ViewDragHelper实现子View拖拽移动的步骤如下:
- 创建ViewDragHelper实例(传入Callback)
- 重写事件拦截处理方法onInterceptTouch和onTouchEvent
- 实现Callback,实现其中的相关方法tryCaptureView以及水平或者垂直方向移动的距离方法
更加具体分析大家可以看前一篇博客,或者我们今天这边会通过具体实例讲解一下。
(三).QQ5.X侧滑效果实现分析:
在正式版本QQ中的侧滑效果如下:
观察上面我们可以理解为两个View,一个是底部的相当于左侧功能View,另外一个是上层主功能内容View,我们在上面进行拖拽上层View或者左右滑动的时候,上层和下层的View相应进行滑动以及View大小变化,同时加入相关的动画。当然我们点击上层的View可以进行打开或者关闭侧滑菜单。
(四).侧滑效果自定义组件实现
1.首先我们这边集成自FrameLayout创建一个自定义View DragLayout。内部的定义的一些变量如下(主要包括一些配置类,手势,ViewDragHelper实例,屏幕宽高,拖拽的子视图View等)
-
- private boolean isShowShadow = true;
-
- private GestureDetectorCompat gestureDetector;
-
- private ViewDragHelper dragHelper;
-
- private DragListener dragListener;
-
- private int range;
-
- private int width;
-
- private int height;
-
- private int mainLeft;
- private Context context;
- private ImageView iv_shadow;
-
- private RelativeLayout vg_left;
-
- private CustomRelativeLayout vg_main;
然后在内部还定义了一个回调接口主要处理拖拽过程中的一些页面打开,关闭以及滑动中的事件回调:
-
-
-
- public interface DragListener {
-
- public void onOpen();
-
- public void onClose();
-
- public void onDrag(float percent);
- }
2.开始创建ViewDragHelper实例,依然在自定义View DragLayout初始化的时候创建,使用ViewDragHelper的静态方法:
- public DragLayout(Context context,AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- gestureDetector = new GestureDetectorCompat(context, new YScrollDetector());
- dragHelper =ViewDragHelper.create(this, dragHelperCallback);
- }
其中create()方法创建的时候传入了一个dragHelperCallBack回调类,将会在第四点中讲到。
3.接着需要重写ViewGroup中事件方法,拦截触摸事件给ViewDragHelper内部进行处理,这样达到拖拽移动子View视图的目的;
-
-
-
-
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- return dragHelper.shouldInterceptTouchEvent(ev) &&gestureDetector.onTouchEvent(ev);
- }
-
-
-
-
-
- @Override
- public boolean onTouchEvent(MotionEvent e){
- try {
- dragHelper.processTouchEvent(e);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- return false;
- }
这边我们在onInterceptTouchEvent拦截让事件从父控件往子View中转移,然后在onTouchEvent方法中拦截让ViewDragHelper进行消费处理。
4.开始自定义创建ViewDragHelper.Callback的实例dragHelperCallback分别实现一个抽象方法tryCaptureView以及重写以下若干个方法来实现侧滑功能,下面一个个来看一下。
-
-
-
-
-
-
- @Override
- public boolean tryCaptureView(View child, int pointerId) {
- return true;
- }
该进行拦截ViewGroup(本例中为:DragLayout)中所有的子View,直接返回true,表示所有的子View都可以进行拖拽移动。
-
-
-
-
-
-
-
- @Override
- public int clampViewPositionHorizontal(View child, int left, int dx) {
- if (mainLeft + dx < 0) {
- return 0;
- } else if (mainLeft + dx >range) {
- return range;
- } else {
- return left;
- }
- }
实现该方法表示水平方向滑动,同时方法中会进行判断边界值,例如当上面的main view已经向左移动边界之外了,直接返回0,表示向左最左边只能x=0;然后向右移动会判断向右最远距离range,至于range的初始化后边会讲到。除了这两种情况之外,就是直接返回left即可。
-
-
-
-
-
- @Override
- public int getViewHorizontalDragRange(View child) {
- return width;
- }
该方法有必要实现,因为该方法在Callback内部默认返回0,也就是说,如果的view的click事件为true,那么会出现整个子View没法拖拽移动的情况了。那么这边直接返回left view宽度了,表示水平方向滑动的最远距离了。
-
-
-
-
-
-
- @Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
- super.onViewReleased(releasedChild,xvel, yvel);
- if (xvel > 0) {
- open();
- } else if (xvel < 0) {
- close();
- } else if (releasedChild == vg_main&& mainLeft > range * 0.3) {
- open();
- } else if (releasedChild == vg_left&& mainLeft > range * 0.7) {
- open();
- } else {
- close();
- }
- }
该方法在拖拽子View移动手指释放的时候被调用,这是会判断移动向左,向右的意图,进行打开或者关闭man view(上层视图)。下面是实现的最后一个方法:onViewPositionChanged
-
-
-
-
-
-
-
-
- @Override
- public void onViewPositionChanged(View changedView, int left, int top,
- int dx, int dy) {
- if (changedView == vg_main) {
- mainLeft = left;
- } else {
- mainLeft = mainLeft + left;
- }
- if (mainLeft < 0) {
- mainLeft = 0;
- } else if (mainLeft > range) {
- mainLeft = range;
- }
-
- if (isShowShadow) {
- iv_shadow.layout(mainLeft, 0,mainLeft + width, height);
- }
- if (changedView == vg_left) {
- vg_left.layout(0, 0, width,height);
- vg_main.layout(mainLeft, 0,mainLeft + width, height);
- }
- dispatchDragEvent(mainLeft);
- }
- };
该方法是在我们进行拖拽移动子View的过程中进行回调,根据移动坐标位置,然后进行重新定义left view和main view。同时调用dispathDragEvent()方法进行拖拽事件相关处理分发同时根据状态来回调接口:
-
-
-
-
- private void dispatchDragEvent(intmainLeft) {
- if (dragListener == null) {
- return;
- }
- float percent = mainLeft / (float)range;
-
- animateView(percent);
-
- dragListener.onDrag(percent);
- Status lastStatus = status;
- if (lastStatus != getStatus()&& status == Status.Close) {
- dragListener.onClose();
- } else if (lastStatus != getStatus()&& status == Status.Open) {
- dragListener.onOpen();
- }
- }
该方法中有一行代码float percent=mainLeft/(float)range;算到一个百分比后面会用到
5.至于子View布局的获取初始化以及宽高和水平滑动距离的大小设置方法:
-
-
-
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- if (isShowShadow) {
- iv_shadow = new ImageView(context);
- iv_shadow.setImageResource(R.mipmap.shadow);
- LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- addView(iv_shadow, 1, lp);
- }
-
- vg_left = (RelativeLayout)getChildAt(0);
-
- vg_main = (CustomRelativeLayout)getChildAt(isShowShadow ? 2 : 1);
- vg_main.setDragLayout(this);
- vg_left.setClickable(true);
- vg_main.setClickable(true);
- }
以及控件大小发生变化回调的方法:
- @Override
- protected void onSizeChanged(int w, int h,int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- width = vg_left.getMeasuredWidth();
- height = vg_left.getMeasuredHeight();
-
- range = (int) (width * 0.6f);
- }
在该方法中我们可以实时获取宽和高以及拖拽水平距离。
6.上面的所有核心代码都为使用ViewDragHelper实现子控件View拖拽移动的方法,但是根据我们这边侧滑效果还需要实现动画以及滑动过程中View的缩放效果,所以我们这边引入了一个动画开源库:该库在Github中大家可以下载下来使用一下,当然后面我会把这个库的使用单独拿出来写一篇文章讲解一下。敬请期待~
然后根据前面算出来的百分比来缩放View视图:
-
-
-
-
- private void animateView(float percent) {
- float f1 = 1 - percent * 0.3f;
-
- ViewHelper.setScaleX(vg_main, f1);
-
- ViewHelper.setScaleY(vg_main, f1);
-
- ViewHelper.setTranslationX(vg_left,-vg_left.getWidth() / 2.3f + vg_left.getWidth() / 2.3f * percent);
-
- ViewHelper.setScaleX(vg_left, 0.5f +0.5f * percent);
-
- ViewHelper.setScaleY(vg_left, 0.5f +0.5f * percent);
-
- ViewHelper.setAlpha(vg_left, percent);
- if (isShowShadow) {
-
- ViewHelper.setScaleX(iv_shadow, f1* 1.4f * (1 - percent * 0.12f));
- ViewHelper.setScaleY(iv_shadow, f1* 1.85f * (1 - percent * 0.12f));
- }
- getBackground().setColorFilter(evaluate(percent, Color.BLACK,Color.TRANSPARENT), Mode.SRC_OVER);
- }
-
- private Integer evaluate(float fraction,Object startValue, Integer endValue) {
- int startInt = (Integer) startValue;
- int startA = (startInt >> 24)& 0xff;
- int startR = (startInt >> 16)& 0xff;
- int startG = (startInt >> 8)& 0xff;
- int startB = startInt & 0xff;
- int endInt = (Integer) endValue;
- int endA = (endInt >> 24) &0xff;
- int endR = (endInt >> 16) &0xff;
- int endG = (endInt >> 8) &0xff;
- int endB = endInt & 0xff;
- return (int) ((startA + (int) (fraction* (endA - startA))) << 24)
- | (int) ((startR + (int)(fraction * (endR - startR))) << 16)
- | (int) ((startG + (int)(fraction * (endG - startG))) << 8)
- | (int) ((startB + (int)(fraction * (endB - startB))));
- }
7.当然除了上面这些还缺少一个效果就是,当我们滑动过程中假如我们手指释放,按照常理来讲view就不会在进行移动了,那么这边我们需要一个加速度当我们释放之后,还能保持一定的速度,该怎么样实现呢?答案就是实现computeScroll()方法。
-
-
-
- @Override
- public void computeScroll() {
- if (dragHelper.continueSettling(true)){
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
OK上面关于DragLayout的核心代码就差不多这么多了,下面是使用DragLayout类来实现侧滑效果啦!
(五).侧滑效果组件使用
1.首先使用的布局文件如下:
该布局文件中父层View就是DragLayout,然后内部有两个RelativeLayout布局,分别充当左侧View(左侧功能)和主Main View。最后在Activity中获取DragLayout控件,添加事件监听器(DragListener)具体代码如下:
- public class MainActivity extends BaseActivity {
- privateDragLayout dl;
- privateListView lv;
- privateTextView tv_noimg;
- privateImageView iv_icon, iv_bottom;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- setStatusBar();
- initDragLayout();
- initView();
-
- }
- private void initDragLayout() {
- dl= (DragLayout) findViewById(R.id.dl);
- dl.setDragListener(new DragLayout.DragListener() {
-
- @Override
- public void onOpen() {
- }
-
- @Override
- public void onClose() {
- }
-
-
- @Override
- public void onDrag(float percent) {
- ViewHelper.setAlpha(iv_icon,1 - percent);
- }
- });
- }
-
- private void initView() {
- iv_icon= (ImageView) findViewById(R.id.iv_icon);
- iv_bottom= (ImageView) findViewById(R.id.iv_bottom);
- tv_noimg= (TextView) findViewById(R.id.iv_noimg);
-
- lv= (ListView) findViewById(R.id.lv);
- lv.setAdapter(new ArrayAdapter<String>(MainActivity.this,
- R.layout.item_text,new String[] { "item 01", "item 01",
- "item01", "item 01", "item 01", "item 01",
- "item01", "item 01", "item 01", "item 01",
- "item01", "item 01", "item 01", "item 01",
- "item01", "item 01", "item 01",
- "item01", "item 01", "item 01", "item 01"}));
- lv.setOnItemClickListener(new OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> arg0, View arg1,
- intposition, long arg3) {
- Toast.makeText(MainActivity.this,"ClickItem "+position,Toast.LENGTH_SHORT).show();
- }
- });
- iv_icon.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View arg0) {
- dl.open();
- }
- });
- }
- @Override
- protectedvoid onResume() {
- super.onResume();
- }
- }
最终运行效果如下:
因为这边底层需要ViewDragHelper类,所以大家在使用的时候需要导入V4包的,不过我这边直接把ViewDragHelper类的源代码复制到项目中了。
(六).DragLayout源代码带注释
上面主要分析DragLayout的具体实现,不过我这边也贴一下DragLayout带有注释的全部源代码让大家可以更好的了解DragLayout的具体实现代码:
-
-
-
- public class DragLayout extends FrameLayout {
- private boolean isShowShadow = true;
-
- private GestureDetectorCompat gestureDetector;
-
- private ViewDragHelper dragHelper;
-
- private DragListener dragListener;
-
- private int range;
-
- private int width;
-
- private int height;
-
- private int mainLeft;
- private Context context;
- private ImageView iv_shadow;
-
- private RelativeLayout vg_left;
-
- private CustomRelativeLayout vg_main;
-
- private Status status = Status.Close;
-
- public DragLayout(Context context) {
- this(context, null);
- }
-
- public DragLayout(Context context,AttributeSet attrs) {
- this(context, attrs, 0);
- this.context = context;
- }
-
- public DragLayout(Context context,AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- gestureDetector = new GestureDetectorCompat(context, new YScrollDetector());
- dragHelper =ViewDragHelper.create(this, dragHelperCallback);
- }
-
- class YScrollDetector extends SimpleOnGestureListener {
- @Override
- public boolean onScroll(MotionEvent e1,MotionEvent e2, float dx, float dy) {
- return Math.abs(dy) <=Math.abs(dx);
- }
- }
-
-
-
-
- private ViewDragHelper.CallbackdragHelperCallback = new ViewDragHelper.Callback() {
-
-
-
-
-
-
-
- @Override
- public int clampViewPositionHorizontal(View child, int left, int dx) {
- if (mainLeft + dx < 0) {
- return 0;
- } else if (mainLeft + dx >range) {
- return range;
- } else {
- return left;
- }
- }
-
-
-
-
-
-
-
- @Override
- public boolean tryCaptureView(View child, int pointerId) {
- return true;
- }
-
-
-
-
-
- @Override
- public int getViewHorizontalDragRange(View child) {
- return width;
- }
-
-
-
-
-
-
-
- @Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
- super.onViewReleased(releasedChild,xvel, yvel);
- if (xvel > 0) {
- open();
- } else if (xvel < 0) {
- close();
- } else if (releasedChild == vg_main&& mainLeft > range * 0.3) {
- open();
- } else if (releasedChild == vg_left&& mainLeft > range * 0.7) {
- open();
- } else {
- close();
- }
- }
-
-
-
-
-
-
-
-
-
- @Override
- public void onViewPositionChanged(View changedView, int left, int top,
- int dx, int dy) {
- if (changedView == vg_main) {
- mainLeft = left;
- } else {
- mainLeft = mainLeft + left;
- }
- if (mainLeft < 0) {
- mainLeft = 0;
- } else if (mainLeft > range) {
- mainLeft = range;
- }
-
- if (isShowShadow) {
- iv_shadow.layout(mainLeft, 0,mainLeft + width, height);
- }
- if (changedView == vg_left) {
- vg_left.layout(0, 0, width,height);
- vg_main.layout(mainLeft, 0,mainLeft + width, height);
- }
-
- dispatchDragEvent(mainLeft);
- }
- };
-
-
-
-
- public interface DragListener {
-
- public void onOpen();
-
- public void onClose();
-
- public void onDrag(float percent);
- }
- public void setDragListener(DragListener dragListener) {
- this.dragListener = dragListener;
- }
-
-
-
-
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- if (isShowShadow) {
- iv_shadow = new ImageView(context);
- iv_shadow.setImageResource(R.mipmap.shadow);
- LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- addView(iv_shadow, 1, lp);
- }
-
- vg_left = (RelativeLayout)getChildAt(0);
-
- vg_main = (CustomRelativeLayout)getChildAt(isShowShadow ? 2 : 1);
- vg_main.setDragLayout(this);
- vg_left.setClickable(true);
- vg_main.setClickable(true);
- }
-
- public ViewGroup getVg_main() {
- return vg_main;
- }
-
- public ViewGroup getVg_left() {
- return vg_left;
- }
-
- @Override
- protected void onSizeChanged(int w, int h,int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- width = vg_left.getMeasuredWidth();
- height = vg_left.getMeasuredHeight();
-
- range = (int) (width * 0.6f);
- }
-
-
-
-
-
-
-
-
-
- @Override
- protected void onLayout(boolean changed,int left, int top, int right, int bottom) {
- vg_left.layout(0, 0, width, height);
- vg_main.layout(mainLeft, 0, mainLeft +width, height);
- }
-
-
-
-
-
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- returndragHelper.shouldInterceptTouchEvent(ev) &&gestureDetector.onTouchEvent(ev);
- }
-
-
-
-
-
-
- @Override
- public boolean onTouchEvent(MotionEvent e){
- try {
- dragHelper.processTouchEvent(e);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- return false;
- }
-
-
-
-
-
- private void dispatchDragEvent(int mainLeft) {
- if (dragListener == null) {
- return;
- }
- float percent = mainLeft / (float)range;
-
- animateView(percent);
-
- dragListener.onDrag(percent);
- Status lastStatus = status;
- if (lastStatus != getStatus()&& status == Status.Close) {
- dragListener.onClose();
- } else if (lastStatus != getStatus()&& status == Status.Open) {
- dragListener.onOpen();
- }
- }
-
-
-
-
-
- private void animateView(float percent) {
- float f1 = 1 - percent * 0.3f;
-
- ViewHelper.setScaleX(vg_main, f1);
-
- ViewHelper.setScaleY(vg_main, f1);
-
- ViewHelper.setTranslationX(vg_left,-vg_left.getWidth() / 2.3f + vg_left.getWidth() / 2.3f * percent);
-
- ViewHelper.setScaleX(vg_left, 0.5f +0.5f * percent);
-
- ViewHelper.setScaleY(vg_left, 0.5f +0.5f * percent);
-
- ViewHelper.setAlpha(vg_left, percent);
- if (isShowShadow) {
-
- ViewHelper.setScaleX(iv_shadow, f1* 1.4f * (1 - percent * 0.12f));
- ViewHelper.setScaleY(iv_shadow, f1* 1.85f * (1 - percent * 0.12f));
- }
- getBackground().setColorFilter(evaluate(percent, Color.BLACK,Color.TRANSPARENT), Mode.SRC_OVER);
- }
-
- private Integer evaluate(float fraction,Object startValue, Integer endValue) {
- int startInt = (Integer) startValue;
- int startA = (startInt >> 24)& 0xff;
- int startR = (startInt >> 16)& 0xff;
- int startG = (startInt >> 8)& 0xff;
- int startB = startInt & 0xff;
- int endInt = (Integer) endValue;
- int endA = (endInt >> 24) &0xff;
- int endR = (endInt >> 16) &0xff;
- int endG = (endInt >> 8) &0xff;
- int endB = endInt & 0xff;
- return (int) ((startA + (int) (fraction* (endA - startA))) << 24)
- | (int) ((startR + (int)(fraction * (endR - startR))) << 16)
- | (int) ((startG + (int)(fraction * (endG - startG))) << 8)
- | (int) ((startB + (int)(fraction * (endB - startB))));
- }
-
-
-
-
- @Override
- public void computeScroll() {
- if (dragHelper.continueSettling(true)){
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
-
-
-
- public enum Status {
- Drag, Open, Close
- }
-
-
-
-
-
- public Status getStatus() {
- if (mainLeft == 0) {
- status = Status.Close;
- } else if (mainLeft == range) {
- status = Status.Open;
- } else {
- status = Status.Drag;
- }
- return status;
- }
-
- public void open() {
- open(true);
- }
-
- public void open(boolean animate) {
- if (animate) {
-
- if(dragHelper.smoothSlideViewTo(vg_main, range, 0)) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
- } else {
- vg_main.layout(range, 0, range * 2,height);
- dispatchDragEvent(range);
- }
- }
-
- public void close() {
- close(true);
- }
-
- public void close(boolean animate) {
- if (animate) {
-
- if(dragHelper.smoothSlideViewTo(vg_main, 0, 0)) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
- } else {
- vg_main.layout(0, 0, width,height);
- dispatchDragEvent(0);
- }
- }
- }
(七).最后总结
今天我们通过ViewDragHelper来讲解实现一个类似QQ5.x侧滑效果的组件的效果,另外该项目中也用到了沉浸式状态的效果。关于该效果的具体实现大家可以看我另一篇文章(点击学习Android沉浸式状态栏)
本次具体实例注释过的全部代码已经上传到Github项目中了。同时欢迎大家去Github站点进行clone或者下载浏览:
https://github.com/jiangqqlmj/ViewDragHelperTest
同时欢迎大家star和fork整个开源快速开发框架项目~
同时致谢: https://github.com/BlueMor/DragLayout
尊重原创,转载请注明:From Sky丶清(http://blog.csdn.net/developer_jiangqq) 侵权必究!
关注我的订阅号,每天分享移动开发技术(Android/IOS),项目管理以及博客文章!