分享

猎豹“快切App”中用到的Android开发技巧探索[附源码]

 quasiceo 2016-08-07
2016-08-05 01:56 1689人阅读 评论(4) 收藏 举报
分类:

版权声明:本文是博主原创文章,未经博主允许的情况下请勿随便转载。顾明伟 http://blog.csdn.net/u013045971

原文链接:http://blog.csdn.net/u013045971/article/details/52119117

前景提要:

        什么是块切?

        快切是从猎豹的Clear Master中分离出来的一个悬浮窗小工具。因为对这个比较感兴趣,博主断断续续花了2个月时间完成了一个类似块切的版本,起了个名字叫“Well Swipe”,中文名叫“Well 划划”。本文会针对Well 划划开发中遇到的一些坑和和技巧做一个分享。来给大家揭密块切开发过程中用到的自定义控件技术细节。在这里还有一个叫“单手划划”的app不得不说,也做的很好。

       块切长啥样子?

       酷安下载地址:http://www./apk/com.well.swipe

       效果图:http://blog.csdn.net/u013045971/article/details/50217903

       Well 划划的gif效果图:https://github.com/gumingwei/WellSwipe/blob/master/app/wellswipe5.gif


问题:

  1. 如何触发菜单
  2. 如何通过手势控制菜单的旋转,打开,关闭
  3. 旋转的过程中如何做到item循环展示
  4. 拖动item效果
  5. 拖动item时的排序效果
  6. item的过渡动画(删除一个item之后,剩余的item会自动平移到目标位置。拖动排序时item自动平移到排序之后的位置)
  7. 控件之间如何交互(旋转菜单的时候指示器跟着转,拖动的时候角落菜单变化状态,拖动到垃圾箱)
  8. 重写的onItemClick,onItemLongClick事件
        带着上述问题开始我们对Well 划划的探索之旅。

1.如何触发菜单
        在设备处于桌面或者其他app的情况下,从屏幕地步往外划来触发菜单。这个真没有别的办法,只能用WindowManager。因为你第三方app不可能拿到桌面或者任何其他app的事件来触发你自己的app。我的做法是在屏幕地步画了6个矩形。一边3个,拼接出来两个L型的区域,不多不少刚刚够用,再在设置里加上调整大小的自定义功能。块切单手划划都是这么搞的。

2.如何通过手势控制菜单的旋转,打开,关闭

        打开:打开这个手势在底部L型的触发区域进行。设计的时候分左右。所以写的时候也要分左右,当手指划过一定距离之后就开始打开菜单,手指这个时候还没停,手指继续滑动的时候计算一个0-1的值用来控制菜单从小到大展开的效果。我设计了这样的一个接口,把需要的scale值回传到菜单view来使用

  1. /**  
  2.  * Created by mingwei on 3/12/16.  
  3.  *  
  4.  *  
  5.  * 微博:     明伟小学生(http://weibo.com/u/2382477985)  
  6.  * Github:   https://github.com/gumingwei  
  7.  * CSDN:     http://blog.csdn.net/u013045971  
  8.  * QQ&WX:   721881283  
  9.  *  
  10.  *  
  11.  */  
  12. public interface OnScaleChangeListener {  
  13.   
  14.     /**  
  15.      * 当scale发生变化的时候回传这个值  
  16.      * <p/>  
  17.      * 1.用于在手指拖动时:                       CatchView.OnEdgeSlidingListener  
  18.      * 2.松开手指时自动打开和关闭的过程中:         AngleLayout.OnOffListener  
  19.      * 3.点击Back键关闭动画的过程中  
  20.      * <改变背景SwipeBackgroundLayout的透明度>  
  21.      *  
  22.      * @param scale  
  23.      */  
  24.     void change(float scale);  
  25. }  

  1. public interface OnEdgeSlidingListener extends OnScaleChangeListener {  
  2.         /**  
  3.          * 打开  
  4.          */  
  5.         void openLeft();//左边打开  
  6.   
  7.         void openRight();//右边打开  
  8.   
  9.         /**  
  10.          * true速度满足自动打开  
  11.          * false速度不满足根据抬手时的状态来判断是否打开  
  12.          * @param view  
  13.          * @param flag  
  14.          */  
  15.         void cancel(View view, boolean flag);  
  16.   
  17.     }  

滑动的过程中持续不断的回传一个scale值。

  1. @Override  
  2. public void change(float scale) {  
  3.         if (mSwipeLayout.hasView()) {  
  4.             if (mSwipeLayout.isSwipeOff()) {  
  5.                 mSwipeLayout.getAngleLayout().setAngleLayoutScale(scale);  
  6.                 mSwipeLayout.setSwipeBackgroundViewAlpha(scale);  
  7.             }  
  8.         }  
  9. }  

        旋转:菜单打开之后,介个时候,手指在菜单的父容器中滑动,回传一个角度值,角度值通过三角函数就可以获得到,菜单跟着旋转。旋转的处理要考虑菜单的打开方式是左边还是右边。后面还加了一个功能,手指往角落方向滑动时关闭菜单,所以还要处理华东时的角度问题。在滑动的过程中回传角度值angle来旋转菜单,松开手指后自动转到目标角度。

  1. public interface OnAngleChangeListener {  
  2.   
  3.         /**  
  4.          * 角度发生变化时传递当前的所显示的数据索引值&当前的百分比  
  5.          * 用于改变Indicator的选中状态,百分比则用来渲染过渡效果  
  6.          *  
  7.          * @param cur 正在显示的是数据index  
  8.          * @param p   百分比  
  9.          */  
  10.         void onAngleChanged(int cur, float p);  
  11.   
  12.     }  
回传的角度用来旋转菜单
  1. @Override  
  2. public void onAngleChanged(int cur, float p) {  
  3.     mIndicator.onAngleChanged2(cur, p);  
  4.     mIndicatorTheme.changeStartAngle(cur, p);  
  5. }  

        关闭:关闭有点击菜单外时,点击角落X时,往角落滑动手指时分别都可以关闭菜单

        1).点击角落XX按钮域时

  1. @Override  
  2.     public void cornerEvent() {  
  3.         if (mEditState == STATE_EDIT) {  
  4.             setEditState(AngleLayout.STATE_NORMAL);  
  5.             return;  
  6.         }  
  7.         if (mEditState == STATE_NORMAL) {  
  8.             off();//点击外部空白区域的时候关闭  
  9.         }  
  10.     }  
       2).点击外部区域
  1. if (mAngleView.isLeft()) {  
  2.                     float upDistance = (float) Math.sqrt(Math.pow((upX - 0), 2) + Math.pow((upY - mHeight), 2));  
  3.                     if (Math.abs(upX - mLastMotionX) < 8 && Math.abs(upY - mLastMotionY) < 8 &&  
  4.                             (upTime - mLastTime) < 200 && (upDistance > mAngleView.getMeasuredHeight())) {  
  5.                         if (mEditState == STATE_EDIT) {  
  6.                             setEditState(AngleLayout.STATE_NORMAL);  
  7.                         } else {  
  8.                             off();  
  9.                         }  
  10.                     }  
  11.                 } else if (mAngleView.isRight()) {  
  12.                     float upDistance = (float) Math.sqrt(Math.pow((upX - mWidth), 2) + Math.pow((upY - mHeight), 2));  
  13.                     if (Math.abs(upX - mLastMotionX) < 8 && Math.abs(upY - mLastMotionY) < 8 &&  
  14.                             (upTime - mLastTime) < 200 && (upDistance > mAngleView.getMeasuredHeight())) {  
  15.                         if (mEditState == STATE_EDIT) {  
  16.                             setEditState(AngleLayout.STATE_NORMAL);  
  17.                         } else {  
  18.                             off();  
  19.                         }  
  20.                     }  
  21.                 }  
        3).快速滑动结束后
  1. if (MOVE_TYPE == TYPE_OFF && upTime - mLastTime < 400) {  
  2.      off();  
  3. }  

3.旋转的过程中如何做到item循环展示

         如何做好循环展示?很简单,每次转90度之后刷新界面,把item重新排序。如初始值是1  2  3

第一次旋转后 2  3  1

第二次旋转后 3  1  2

第三次旋转后 1  2  3   这时又回到了原位,每四次是一个循环,这样规律久很容易总结出来了。

当前的数据索引index

  1. /**  
  2.      * 根据index获取当先index所需要的数据索引  
  3.      * 比如: 11->1,10->2,9->0,8->1,7->2,6->0  像这样一直循环  
  4.      *  
  5.      * @param index 转动结束后根据BaseAngle的值除以90得出的范围0-11  
  6.      *              3,4的最小公倍数的是12  
  7.      * @return  
  8.      */  
  9.     private int getViewsIndex(int index) {  
  10.         return (COUNT_12 - index) % COUNT_3;  
  11.     }  
上一组的索引
  1. /**  
  2.      * 上一个数据索引  
  3.      *  
  4.      * @param index 传入的是getViews()的返回值  
  5.      * @return  
  6.      */  
  7.     public int getPreViewsIndex(int index) {  
  8.         return index == 0 ? 2 : (index - 1);  
  9.     }  
下一组索引
  1. /**  
  2.      * 下一个数据索引  
  3.      *  
  4.      * @param index 传入的是getViews()的返回值  
  5.      * @return  
  6.      */  
  7.     public int getNextViewsIndex(int index) {  
  8.         return index == 2 ? 0 : (index + 1);  
  9.     }  
求出因该取那一组数据之后,还要求出哪个限象,即,把第X组数据放在第Y限象。然后一只重复起来就造成了循环的感觉

当前限象currentQua

  1. /**  
  2.      * 根据当前的index获取当前显示限象index  
  3.      * 比如11->1,10->2,9->3,8->0  
  4.      *  
  5.      * @param index 转动结束后根据BaseAngle的值除以90得出的范围0-11  
  6.      *              3,4的最小公倍数的是12  
  7.      * @return  
  8.      */  
  9.     public int getQuaIndex(int index) {  
  10.         return index == 0 ? 0 : (COUNT_12 - index) % COUNT_4;  
  11.     }  
上一个限象preQua
  1. /**  
  2.      * 获取当前index的上一个index  
  3.      *  
  4.      * @param index 传入的是getIndex()的返回值  
  5.      * @return 得到上一个index  
  6.      */  
  7.     public int getPreQuaIndex(int index) {  
  8.         return index == 0 ? COUNT_3 : (index - 1);  
  9.     }  
下一个限象nextQua
  1. /**  
  2.      * 获取当前index的下一个index  
  3.      *  
  4.      * @param index 传入的是getIndex()的返回值  
  5.      * @return 得到下一个index  
  6.      */  
  7.     private int getNextQuaIndex(int index) {  
  8.         return index == COUNT_3 ? 0 : (index + 1);  
  9.     }  
最后拿求得的数据喝限象来布局item

布局子item的位置

  1. /**  
  2.      * 布局子控件  
  3.      * 通过一个当前值,计算上一个,下一个值  
  4.      *  
  5.      * @param index  
  6.      */  
  7.     private void itemLayout(int index) {  
  8.         mCurrentIndex = getRealIndex(index);  
  9.         itemLayout(mMap.get(getPreViewsIndex(getViewsIndex(index))), getPreQuaIndex(getQuaIndex(index)));//上一组  
  10.         itemLayout(mMap.get(getViewsIndex(index)), getQuaIndex(index));//当前组  
  11.         itemLayout(mMap.get(getNextViewsIndex(getViewsIndex(index))), getNextQuaIndex(getQuaIndex(index)));//下一组  
  12.     }  

4.拖动item效果

拖动Item这个其实也没啥难的,就是长按的时候根据按下的位置和item布局的时候求得位置来得到一个Item的对象,拿到item的数据,在父容器中创建一个view跟着手指移动就可以了。有人就问了,直接onItemLongClick不就可以了,为啥还要求啊,这个还真是要求的,控件角度发生变化后的onClick,onItem等事件触发是有问题的,因为画布发生了变化,点击能找到子Item,但是找的不对,item已经转走了,事件还可以触发,这样就不合适了不是,所以我们要重写一系列事件。

拖拽的时候传递item的数据信息,包括视图view,坐标信息,offsetLeft,offsetTop

  1. public interface OnEditModeChangeListener {  
  2.         /**  
  3.          * 进入编辑模式  
  4.          *  
  5.          * @param view  
  6.          */  
  7.         void onEnterEditMode(View view);  
  8.   
  9.         /**  
  10.          * 退出编辑模式  
  11.          */  
  12.         void onExitEditMode();  
  13.   
  14.         /**  
  15.          * 进入拖拽模式  
  16.          *  
  17.          * @param view       判定进行拖拽的当前view  
  18.          * @param left       在父控件中的left值  
  19.          * @param top        在父控件中的top值  
  20.          * @param offsetLeft 触摸点在当前进行拖拽的view的left距离  
  21.          * @param offsetTop  触摸点在当前进行拖拽的view的top距离  
  22.          */  
  23.         void onStartDrag(AngleItemCommon view, float left, float top, float offsetLeft, float offsetTop);  
  24.   
  25.         /**  
  26.          * 拖拽取消  
  27.          */  
  28.         void onCancelDrag();  
  29.     }  
拖拽结束的时候调用
  1. public interface OnItemDragListener {  
  2.   
  3.         /**  
  4.          * 拖拽结束时调用  
  5.          *  
  6.          * @param index 返回当前的数据索引index  
  7.          */  
  8.         void onDragEnd(int index);  
  9.     }  

5.拖动item时的排序效果

6.item的过渡动画(删除一个item之后,剩余的item会自动平移到目标位置。拖动排序时item自动平移到排序之后的位置)

5和6可以放在一起说,不管是删除还是排序,基本上就是数据发生变化之后再做视图View变换效果的过程,计算位置的算法即全面已经写好的,在这里取掉限象的计算就可以使用了。

例如:123456  这几个数据初始化完成之后会有一个自身的位置。然后把位置信息保存在一个list中。

比如现在删除掉3,就剩下12456,这时候快速的用位置计算算法再算出只有5个item时的新的一个位置信息list,计算完成后遍历12456,遍历平移对应的位置信息(这里用属性动画就可以),位置没有发生变化的就不会动,位置信息变化的就有平移效果。

位置平移在排序和删除的时候的应用时一样的。

平移动画

  1. /**  
  2.      * 移除item之后的过渡动画  
  3.      * 从原始坐标移动到新坐标  
  4.      *  
  5.      * @param resource   移除控件之后计算产生的新的item坐标  
  6.      * @param targetView 原始坐标  
  7.      */  
  8.     public void transAnimator(final ArrayList<Coordinate> resource, final ArrayList<AngleItemCommon> targetView) {  
  9.         ValueAnimator translation = ValueAnimator.ofFloat(0f, 1f);  
  10.         translation.setDuration(250);  
  11.         translation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  12.             @Override  
  13.             public void onAnimationUpdate(ValueAnimator animation) {  
  14.                 float values = (float) animation.getAnimatedValue();  
  15.                 for (int i = 0; i < targetView.size(); i++) {  
  16.                     float x = (float) (resource.get(i).x - targetView.get(i).getParentX()) * values;  
  17.                     float y = (float) (resource.get(i).y - targetView.get(i).getParentY()) * values;  
  18.                     targetView.get(i).setTranslationX(x);  
  19.                     targetView.get(i).setTranslationY(y);  
  20.                     requestLayout();  
  21.                 }  
  22.             }  
  23.         });  
  24.         translation.addListener(new Animator.AnimatorListener() {  
  25.             @Override  
  26.             public void onAnimationStart(Animator animation) {  
  27.   
  28.             }  
  29.   
  30.             @Override  
  31.             public void onAnimationEnd(Animator animation) {  
  32.                 for (int i = 0; i < targetView.size(); i++) {  
  33.                     targetView.get(i).setTranslationX(0);  
  34.                     targetView.get(i).setTranslationY(0);  
  35.                 }  
  36.                 getData().remove(mTargetItem);  
  37.                 /**  
  38.                  * 判断最后一个Item,也就是只剩加号的时候退出编辑状态  
  39.                  */  
  40.                 if (getData().size() == 1) {  
  41.                     mOnEditModeChangeListener.onExitEditMode();  
  42.                 }  
  43.   
  44.                 isRemoveFinish = true;  
  45.             }  
  46.   
  47.             @Override  
  48.             public void onAnimationCancel(Animator animation) {  
  49.   
  50.             }  
  51.   
  52.             @Override  
  53.             public void onAnimationRepeat(Animator animation) {  
  54.   
  55.             }  
  56.         });  
  57.         translation.start();  
  58.     }  
交换动画
  1. /**  
  2.      * 交换动画  
  3.      *  
  4.      * @param resource   源坐标,也就是动画的起始坐标  
  5.      * @param targetView 目标坐标,也就是动画的终点坐标  
  6.      * @param index      当前view交换之后的目标位置的index索引,主要用来屏蔽动画,因为松手之后有动画,不需要这这里再加动画了  
  7.      */  
  8.     public void exchangeAnimator(final Coordinate resource, final ArrayList<AngleItemCommon> targetView, final int index) {  
  9.         ValueAnimator translation = ValueAnimator.ofFloat(0f, 1f);  
  10.         translation.setDuration(250);  
  11.         translation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  12.             @Override  
  13.             public void onAnimationUpdate(ValueAnimator animation) {  
  14.                 float values = (float) animation.getAnimatedValue();  
  15.                 float x = (float) (resource.x - targetView.get(index).getParentX()) * values;  
  16.                 float y = (float) (resource.y - targetView.get(index).getParentY()) * values;  
  17.                 targetView.get(index).setTranslationX(x);  
  18.                 targetView.get(index).setTranslationY(y);  
  19.                 requestLayout();  
  20.             }  
  21.         });  
  22.         translation.addListener(new Animator.AnimatorListener() {  
  23.             @Override  
  24.             public void onAnimationStart(Animator animation) {  
  25.   
  26.             }  
  27.   
  28.             @Override  
  29.             public void onAnimationEnd(Animator animation) {  
  30.                 for (int i = 0; i < targetView.size(); i++) {  
  31.                     targetView.get(i).setTranslationX(0);  
  32.                     targetView.get(i).setTranslationY(0);  
  33.                 }  
  34.                 putData(targetView);  
  35.                 isExChangeFinish = true;  
  36.   
  37.             }  
  38.   
  39.             @Override  
  40.             public void onAnimationCancel(Animator animation) {  
  41.   
  42.             }  
  43.   
  44.             @Override  
  45.             public void onAnimationRepeat(Animator animation) {  
  46.   
  47.             }  
  48.         });  
  49.         translation.start();  
  50.     }  


7.控件之间如何交互

AngleView  菜单                                    (关键代码近2000行)

AngleLayout 菜单容器

AngleViewTheme 菜单主题

AngleIndicatorView 菜单指示器

AnglrIndicatorTheme 菜单指示器主题 

CornerView 角落控制按钮

CornerThemeView 角落控制按钮主题

LoadingView 进度条 


各个控件都定义了接口和其他接口进行值的传递和交互

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <com.well.swipe.view.AngleLayout xmlns:android="http://schemas./apk/res/android"  
  3.     android:id="@+id/anglelayout"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent">  
  6.   
  7.     <com.well.swipe.view.AngleViewTheme  
  8.         android:id="@id/angleview_theme"  
  9.         android:layout_width="@dimen/angleview_size"  
  10.         android:layout_height="@dimen/angleview_size" />  
  11.   
  12.     <com.well.swipe.view.AngleIndicatorViewTheme  
  13.         android:id="@id/indicator_theme"  
  14.         android:layout_width="@dimen/angleindicator_theme_size"  
  15.         android:layout_height="@dimen/angleindicator_theme_size" />  
  16.   
  17.     <com.well.swipe.view.AngleIndicatorView  
  18.         android:id="@id/indicator"  
  19.         android:layout_width="@dimen/angleindicator_size"  
  20.         android:layout_height="@dimen/angleindicator_size" />  
  21.   
  22.     <com.well.swipe.view.AngleView  
  23.         android:id="@id/angleview"  
  24.         android:layout_width="@dimen/angleview_size"  
  25.         android:layout_height="@dimen/angleview_size" />  
  26.   
  27.     <com.well.swipe.view.CornerThemeView  
  28.         android:id="@+id/corner_theme"  
  29.         android:layout_width="@dimen/anglelogo_size"  
  30.         android:layout_height="@dimen/anglelogo_size" />  
  31.   
  32.     <com.well.swipe.view.CornerView  
  33.         android:id="@+id/corner_view"  
  34.         android:layout_width="@dimen/anglelogo_size"  
  35.         android:layout_height="@dimen/anglelogo_size" />  
  36.   
  37.     <com.well.swipe.view.LoadingView xmlns:loading="http://schemas./apk/res-auto"  
  38.         android:id="@+id/recent_loading"  
  39.         android:layout_width="20dp"  
  40.         android:layout_height="20dp"  
  41.         android:padding="10dp"  
  42.         loading:inner_color="@color/preference_title_enable_color"  
  43.         loading:inner_rotating_speed="5"  
  44.         loading:inner_width="2dip"  
  45.         loading:outer_color="@color/check_item_enable"  
  46.         loading:outer_width="2dip"/>  
  47.   
  48. </com.well.swipe.view.AngleLayout>  

8.重写的onItemClick,onItemLongClick事件

为什么要重写onClick,onItemClick,onItemLongClick呢?原生的好好的为什么不用?

这个问题其实前面已经给出了答案,当控件的画布发生变化之后,这些“on事件”是可以触发的,但是看不到实际的item控件,因为这些控件已经跟着画布转走了,再使用肯定不是办法,所以要重写,重写的时候根据子item布局时候的位置加上onTouch的位置信息即可计算出来点击的是哪个item。所以自定义这些"on事件"也不是什么难事。


如果你还想进一步了解Well划划的更多代码细节,请移步到Github,上面有完整的源码。

如何支持开发者?

可以这样->进入酷安或者Play下载安装,送上5星好评

酷安:http://www./apk/com.well.swipe

Play:https://play.google.com/store/apps/details?id=com.well.swipe&hl=zh



关于我:

/**
 * Created by mingwei on 2/25/16.
 *
 * 微博:     明伟小学生(http://weibo.com/u/2382477985)
 * Github:   https://github.com/gumingwei
 * CSDN:     http://blog.csdn.net/u013045971
 * QQ&WX:   721881283
 *
 */


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多