本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发
原文链接:http://blog.csdn.net/u013045971/article/details/52119117
前景提要:
什么是块切?
快切是从猎豹的Clear Master中分离出来的一个悬浮窗小工具。因为对这个比较感兴趣,博主断断续续花了2个月时间完成了一个类似块切的版本,起了个名字叫“Well Swipe”,中文名叫“Well 划划”。本文会针对Well 划划开发中遇到的一些坑和和技巧做一个分享。来给大家揭密块切开发过程中用到的自定义控件技术细节。在这里还有一个叫“单手划划”的app不得不说,也做的很好。
块切长啥样子?
酷安下载地址:http://www.coolapk.com/apk/com.well.swipe
效果图:http://blog.csdn.net/u013045971/article/details/50217903
Well 划划的gif效果图:https://github.com/gumingwei/WellSwipe/blob/master/app/wellswipe5.gif
问题:
打开:打开这个手势在底部L型的触发区域进行。设计的时候分左右。所以写的时候也要分左右,当手指划过一定距离之后就开始打开菜单,手指这个时候还没停,手指继续滑动的时候计算一个0-1的值用来控制菜单从小到大展开的效果。我设计了这样的一个接口,把需要的scale值回传到菜单view来使用
/**
* Created by mingwei on 3/12/16.
*
*
* 微博: 明伟小学生(http://weibo.com/u/2382477985)
* Github: https://github.com/gumingwei
* CSDN: http://blog.csdn.net/u013045971
* QQ&WX: 721881283
*
*
*/
public interface OnScaleChangeListener {
/**
* 当scale发生变化的时候回传这个值
* <p/>
* 1.用于在手指拖动时: CatchView.OnEdgeSlidingListener
* 2.松开手指时自动打开和关闭的过程中: AngleLayout.OnOffListener
* 3.点击Back键关闭动画的过程中
* <改变背景SwipeBackgroundLayout的透明度>
*
* @param scale
*/
void change(float scale);
}
public interface OnEdgeSlidingListener extends OnScaleChangeListener {
/**
* 打开
*/
void openLeft();//左边打开
void openRight();//右边打开
/**
* true速度满足自动打开
* false速度不满足根据抬手时的状态来判断是否打开
* @param view
* @param flag
*/
void cancel(View view, boolean flag);
}
滑动的过程中持续不断的回传一个scale值。
@Override
public void change(float scale) {
if (mSwipeLayout.hasView()) {
if (mSwipeLayout.isSwipeOff()) {
mSwipeLayout.getAngleLayout().setAngleLayoutScale(scale);
mSwipeLayout.setSwipeBackgroundViewAlpha(scale);
}
}
}
旋转:菜单打开之后,介个时候,手指在菜单的父容器中滑动,回传一个角度值,角度值通过三角函数就可以获得到,菜单跟着旋转。旋转的处理要考虑菜单的打开方式是左边还是右边。后面还加了一个功能,手指往角落方向滑动时关闭菜单,所以还要处理华东时的角度问题。在滑动的过程中回传角度值angle来旋转菜单,松开手指后自动转到目标角度。
public interface OnAngleChangeListener {
/**
* 角度发生变化时传递当前的所显示的数据索引值&当前的百分比
* 用于改变Indicator的选中状态,百分比则用来渲染过渡效果
*
* @param cur 正在显示的是数据index
* @param p 百分比
*/
void onAngleChanged(int cur, float p);
}
回传的角度用来旋转菜单
@Override
public void onAngleChanged(int cur, float p) {
mIndicator.onAngleChanged2(cur, p);
mIndicatorTheme.changeStartAngle(cur, p);
}
关闭:关闭有点击菜单外时,点击角落X时,往角落滑动手指时分别都可以关闭菜单
1).点击角落XX按钮域时
@Override
public void cornerEvent() {
if (mEditState == STATE_EDIT) {
setEditState(AngleLayout.STATE_NORMAL);
return;
}
if (mEditState == STATE_NORMAL) {
off();//点击外部空白区域的时候关闭
}
}
2).点击外部区域
if (mAngleView.isLeft()) {
float upDistance = (float) Math.sqrt(Math.pow((upX - 0), 2) + Math.pow((upY - mHeight), 2));
if (Math.abs(upX - mLastMotionX) < 8 && Math.abs(upY - mLastMotionY) < 8 &&
(upTime - mLastTime) < 200 && (upDistance > mAngleView.getMeasuredHeight())) {
if (mEditState == STATE_EDIT) {
setEditState(AngleLayout.STATE_NORMAL);
} else {
off();
}
}
} else if (mAngleView.isRight()) {
float upDistance = (float) Math.sqrt(Math.pow((upX - mWidth), 2) + Math.pow((upY - mHeight), 2));
if (Math.abs(upX - mLastMotionX) < 8 && Math.abs(upY - mLastMotionY) < 8 &&
(upTime - mLastTime) < 200 && (upDistance > mAngleView.getMeasuredHeight())) {
if (mEditState == STATE_EDIT) {
setEditState(AngleLayout.STATE_NORMAL);
} else {
off();
}
}
}
3).快速滑动结束后
if (MOVE_TYPE == TYPE_OFF && upTime - mLastTime < 400) {
off();
}
如何做好循环展示?很简单,每次转90度之后刷新界面,把item重新排序。如初始值是1 2 3
第一次旋转后 2 3 1
第二次旋转后 3 1 2
第三次旋转后 1 2 3 这时又回到了原位,每四次是一个循环,这样规律久很容易总结出来了。
当前的数据索引index
/**
* 根据index获取当先index所需要的数据索引
* 比如: 11->1,10->2,9->0,8->1,7->2,6->0 像这样一直循环
*
* @param index 转动结束后根据BaseAngle的值除以90得出的范围0-11
* 3,4的最小公倍数的是12
* @return
*/
private int getViewsIndex(int index) {
return (COUNT_12 - index) % COUNT_3;
}
上一组的索引
/**
* 上一个数据索引
*
* @param index 传入的是getViews()的返回值
* @return
*/
public int getPreViewsIndex(int index) {
return index == 0 ? 2 : (index - 1);
}
下一组索引
/**
* 下一个数据索引
*
* @param index 传入的是getViews()的返回值
* @return
*/
public int getNextViewsIndex(int index) {
return index == 2 ? 0 : (index + 1);
}
求出因该取那一组数据之后,还要求出哪个限象,即,把第X组数据放在第Y限象。然后一只重复起来就造成了循环的感觉
当前限象currentQua
/**
* 根据当前的index获取当前显示限象index
* 比如11->1,10->2,9->3,8->0
*
* @param index 转动结束后根据BaseAngle的值除以90得出的范围0-11
* 3,4的最小公倍数的是12
* @return
*/
public int getQuaIndex(int index) {
return index == 0 ? 0 : (COUNT_12 - index) % COUNT_4;
}
上一个限象preQua
/**
* 获取当前index的上一个index
*
* @param index 传入的是getIndex()的返回值
* @return 得到上一个index
*/
public int getPreQuaIndex(int index) {
return index == 0 ? COUNT_3 : (index - 1);
}
下一个限象nextQua
/**
* 获取当前index的下一个index
*
* @param index 传入的是getIndex()的返回值
* @return 得到下一个index
*/
private int getNextQuaIndex(int index) {
return index == COUNT_3 ? 0 : (index + 1);
}
最后拿求得的数据喝限象来布局item
布局子item的位置
/**
* 布局子控件
* 通过一个当前值,计算上一个,下一个值
*
* @param index
*/
private void itemLayout(int index) {
mCurrentIndex = getRealIndex(index);
itemLayout(mMap.get(getPreViewsIndex(getViewsIndex(index))), getPreQuaIndex(getQuaIndex(index)));//上一组
itemLayout(mMap.get(getViewsIndex(index)), getQuaIndex(index));//当前组
itemLayout(mMap.get(getNextViewsIndex(getViewsIndex(index))), getNextQuaIndex(getQuaIndex(index)));//下一组
}
拖动Item这个其实也没啥难的,就是长按的时候根据按下的位置和item布局的时候求得位置来得到一个Item的对象,拿到item的数据,在父容器中创建一个view跟着手指移动就可以了。有人就问了,直接onItemLongClick不就可以了,为啥还要求啊,这个还真是要求的,控件角度发生变化后的onClick,onItem等事件触发是有问题的,因为画布发生了变化,点击能找到子Item,但是找的不对,item已经转走了,事件还可以触发,这样就不合适了不是,所以我们要重写一系列事件。
拖拽的时候传递item的数据信息,包括视图view,坐标信息,offsetLeft,offsetTop
public interface OnEditModeChangeListener {
/**
* 进入编辑模式
*
* @param view
*/
void onEnterEditMode(View view);
/**
* 退出编辑模式
*/
void onExitEditMode();
/**
* 进入拖拽模式
*
* @param view 判定进行拖拽的当前view
* @param left 在父控件中的left值
* @param top 在父控件中的top值
* @param offsetLeft 触摸点在当前进行拖拽的view的left距离
* @param offsetTop 触摸点在当前进行拖拽的view的top距离
*/
void onStartDrag(AngleItemCommon view, float left, float top, float offsetLeft, float offsetTop);
/**
* 拖拽取消
*/
void onCancelDrag();
}
拖拽结束的时候调用
public interface OnItemDragListener {
/**
* 拖拽结束时调用
*
* @param index 返回当前的数据索引index
*/
void onDragEnd(int index);
}
6.item的过渡动画(删除一个item之后,剩余的item会自动平移到目标位置。拖动排序时item自动平移到排序之后的位置)
5和6可以放在一起说,不管是删除还是排序,基本上就是数据发生变化之后再做视图View变换效果的过程,计算位置的算法即全面已经写好的,在这里取掉限象的计算就可以使用了。
例如:123456 这几个数据初始化完成之后会有一个自身的位置。然后把位置信息保存在一个list中。
比如现在删除掉3,就剩下12456,这时候快速的用位置计算算法再算出只有5个item时的新的一个位置信息list,计算完成后遍历12456,遍历平移对应的位置信息(这里用属性动画就可以),位置没有发生变化的就不会动,位置信息变化的就有平移效果。
位置平移在排序和删除的时候的应用时一样的。
平移动画
/**
* 移除item之后的过渡动画
* 从原始坐标移动到新坐标
*
* @param resource 移除控件之后计算产生的新的item坐标
* @param targetView 原始坐标
*/
public void transAnimator(final ArrayList<Coordinate> resource, final ArrayList<AngleItemCommon> targetView) {
ValueAnimator translation = ValueAnimator.ofFloat(0f, 1f);
translation.setDuration(250);
translation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float values = (float) animation.getAnimatedValue();
for (int i = 0; i < targetView.size(); i++) {
float x = (float) (resource.get(i).x - targetView.get(i).getParentX()) * values;
float y = (float) (resource.get(i).y - targetView.get(i).getParentY()) * values;
targetView.get(i).setTranslationX(x);
targetView.get(i).setTranslationY(y);
requestLayout();
}
}
});
translation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
for (int i = 0; i < targetView.size(); i++) {
targetView.get(i).setTranslationX(0);
targetView.get(i).setTranslationY(0);
}
getData().remove(mTargetItem);
/**
* 判断最后一个Item,也就是只剩加号的时候退出编辑状态
*/
if (getData().size() == 1) {
mOnEditModeChangeListener.onExitEditMode();
}
isRemoveFinish = true;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
translation.start();
}
交换动画
/**
* 交换动画
*
* @param resource 源坐标,也就是动画的起始坐标
* @param targetView 目标坐标,也就是动画的终点坐标
* @param index 当前view交换之后的目标位置的index索引,主要用来屏蔽动画,因为松手之后有动画,不需要这这里再加动画了
*/
public void exchangeAnimator(final Coordinate resource, final ArrayList<AngleItemCommon> targetView, final int index) {
ValueAnimator translation = ValueAnimator.ofFloat(0f, 1f);
translation.setDuration(250);
translation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float values = (float) animation.getAnimatedValue();
float x = (float) (resource.x - targetView.get(index).getParentX()) * values;
float y = (float) (resource.y - targetView.get(index).getParentY()) * values;
targetView.get(index).setTranslationX(x);
targetView.get(index).setTranslationY(y);
requestLayout();
}
});
translation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
for (int i = 0; i < targetView.size(); i++) {
targetView.get(i).setTranslationX(0);
targetView.get(i).setTranslationY(0);
}
putData(targetView);
isExChangeFinish = true;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
translation.start();
}
AngleView 菜单 (关键代码近2000行)
AngleLayout 菜单容器
AngleViewTheme 菜单主题
AngleIndicatorView 菜单指示器
AnglrIndicatorTheme 菜单指示器主题
CornerView 角落控制按钮
CornerThemeView 角落控制按钮主题
LoadingView 进度条
各个控件都定义了接口和其他接口进行值的传递和交互
<?xml version="1.0" encoding="utf-8"?>
<com.well.swipe.view.AngleLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/anglelayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.well.swipe.view.AngleViewTheme
android:id="@id/angleview_theme"
android:layout_width="@dimen/angleview_size"
android:layout_height="@dimen/angleview_size" />
<com.well.swipe.view.AngleIndicatorViewTheme
android:id="@id/indicator_theme"
android:layout_width="@dimen/angleindicator_theme_size"
android:layout_height="@dimen/angleindicator_theme_size" />
<com.well.swipe.view.AngleIndicatorView
android:id="@id/indicator"
android:layout_width="@dimen/angleindicator_size"
android:layout_height="@dimen/angleindicator_size" />
<com.well.swipe.view.AngleView
android:id="@id/angleview"
android:layout_width="@dimen/angleview_size"
android:layout_height="@dimen/angleview_size" />
<com.well.swipe.view.CornerThemeView
android:id="@+id/corner_theme"
android:layout_width="@dimen/anglelogo_size"
android:layout_height="@dimen/anglelogo_size" />
<com.well.swipe.view.CornerView
android:id="@+id/corner_view"
android:layout_width="@dimen/anglelogo_size"
android:layout_height="@dimen/anglelogo_size" />
<com.well.swipe.view.LoadingView xmlns:loading="http://schemas.android.com/apk/res-auto"
android:id="@+id/recent_loading"
android:layout_width="20dp"
android:layout_height="20dp"
android:padding="10dp"
loading:inner_color="@color/preference_title_enable_color"
loading:inner_rotating_speed="5"
loading:inner_width="2dip"
loading:outer_color="@color/check_item_enable"
loading:outer_width="2dip"/>
</com.well.swipe.view.AngleLayout>
为什么要重写onClick,onItemClick,onItemLongClick呢?原生的好好的为什么不用?
这个问题其实前面已经给出了答案,当控件的画布发生变化之后,这些“on事件”是可以触发的,但是看不到实际的item控件,因为这些控件已经跟着画布转走了,再使用肯定不是办法,所以要重写,重写的时候根据子item布局时候的位置加上onTouch的位置信息即可计算出来点击的是哪个item。所以自定义这些"on事件"也不是什么难事。
如果你还想进一步了解Well划划的更多代码细节,请移步到Github,上面有完整的源码 源码链接。
如何支持开发者?
可以这样->进入酷安或者Play下载安装,送上5星好评
酷安:http://www.coolapk.com/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 * */