深入剖析锤子onestep代码实现 - 下篇 - 拖拽分享
onestep拖拽分享功能代码分析
文章结构
- 拖拽原理
- 拖拽事件派发与处理 - ACTION_DRAG_STARTED, ACTION_DRAG_ENTERED, ACTION_DROP, ACTION_DRAG_EXITED
- 拖拽结束,ACTION_DRAG_END事件处理分析
- 拖拽移动,ACTION_DRAG_LOCATION事件处理分析
拖拽原理
- 拖拽原理移步到SDK文档,drag-drop.html, TODO: 待翻译
这里从拖拽事件派发到onestep窗口时开始分析。由于时序图太长,因此分段分析。
拖拽事件派发与处理 - ACTION_DRAG_STARTED, ACTION_DRAG_ENTERED, ACTION_DROP, ACTION_DRAG_EXITED
备注:
SideView实际上包含了多个SidebarListView的子视图实例(见上篇),这里仅以sharelist子视图为例来分析和画图,layout为
<com.smartisanos.sidebar.view.SidebarListView
android:id="@+id/sharelist"
与其配套的相关类为ResolveInfoListAdapter, ResolveInfoGroup, ResolveInfoManager


ViewRootImpl会按View层级,往下调用视图的dispatchDragEvent方法派发拖拽事。
即window->viewroot->decor->...->sideview->...
SideView重写了dispatchDragEvent()方法,并对ACTION_DRAG_STARTED进行了处理:
class SideView{
@Override
public boolean dispatchDragEvent(DragEvent event) {
case DragEvent.ACTION_DRAG_STARTED:
FloatText.getInstance(mContext).start();//图标悬浮文字
onDragStart(event);//拖拽前的预备动作,下述
//最终回调到ViewGroup.dispatchDragEvent(),让其执行默认派发逻辑,
//最终会遍历所有的子视图,调用其Drag事件监听器
return super.dispatchDragEvent(event);
... ...
}
//拖拽前的预备动作, 切换普通模式显示列表和分享组件列表
//普通模式,onestep刚开始显示时的状态,显示APP图标、联系人,此时你可以切换应用,点击图标打开应用、联系人
//拖拽分享模式,比如拖拽一堆图片要分享给联系人或目标应用
private void onDragStart(final DragEvent event) {
//停止正在进行的切换动画
if (mSwitchContentAnim != null) {
mSwitchContentAnim.cancel();
}
int deltaWidth = mContext.getResources().getDimensionPixelSize(R.dimen.sidebar_list_anim_padding);
boolean leftMode = (SidebarController.getInstance(mContext).getSidebarMode() == SidebarMode.MODE_LEFT);
int width = getWidth() + deltaWidth;
int outTo = leftMode ? -width : width;
//新建切换动画
mSwitchContentAnim = new AnimTimeLine();
int time = 300;
final List<View> disappearViews = new ArrayList<View>();
//需要消失的图标,即普通模式下显示时的三个SidebarListView列表里的APP图标、联系人图标
disappearViews.addAll(mOngoingList.getViewList());
disappearViews.addAll(mContactList.getViewList());
disappearViews.addAll(mAppList.getViewList());
if (disappearViews.size() > 0) {
Vector3f scaleFrom = new Vector3f(1, 1);
Vector3f scaleTo = new Vector3f(0.2f, 0.2f);
Vector3f alphaFrom = new Vector3f(0, 0, 1);
Vector3f alphaTo = new Vector3f(0, 0, 0);
int count = disappearViews.size();
for (int i = 0; i < count; i++) {
View view = disappearViews.get(i);
Anim scale = new Anim(view, Anim.SCALE, time, Anim.CUBIC_OUT, scaleFrom, scaleTo);
Anim alpha = new Anim(view, Anim.TRANSPARENT, time, Anim.CUBIC_OUT, alphaFrom, alphaTo);
//动画效果,透明度渐变为0,缩小
mSwitchContentAnim.addAnim(scale);
mSwitchContentAnim.addAnim(alpha);
}
}
//更新支持分享的图标列表,见下文
if (event != null) {
mOngoingListFake.onDragStart(event);
mContactListFake.onDragStart(event);
mShareList.onDragStart(event);
}
mScrollViewDragged.setTranslationX(outTo);
Anim inAnim = new Anim(mScrollViewDragged, Anim.MOVE, time, Anim.CUBIC_OUT, new Vector3f(outTo, 0), new Vector3f());
inAnim.setDelay(time / 4);
mSwitchContentAnim.addAnim(inAnim);
mSwitchContentAnim.setAnimListener(new AnimListener() {
@Override
public void onStart() {
mScrollViewDragged.setVisibility(VISIBLE);
}
//切换动画结束
@Override
public void onComplete(int type) {
if (mSwitchContentAnim != null) {
int count = disappearViews.size();
for (int i = 0; i < count; i++) {
View view = disappearViews.get(i);
view.setAlpha(1);
view.setScaleX(1);
view.setScaleY(1);
}
mScrollViewNormal.setVisibility(GONE);//普通模式的列表消失,不显示
mScrollViewDragged.setVisibility(VISIBLE);//显示用于分享用的列表
mScrollViewDragged.setTranslationX(0);
mSwitchContentAnim = null;
}
}
});
mSwitchContentAnim.start();//切换动画开始
}
}
列表视图的处理:
class SidebarListView{
public void onDragEnd() {
if (mAdapter != null) {
mAdapter.onDragStart();//转给适配器处理
}
}
}
ResolveInfoListAdapter的处理:
class ResolveInfoListAdapter{
@Override
public void onDragStart(DragEvent event) {
... ...
updateAcceptableResolveInfos();
}
//更新支持分享的图标列表
private void updateAcceptableResolveInfos() {
mAcceptableResolveInfos.clear();
for (ResolveInfoGroup rig : mResolveInfos) {
//acceptDragEvent原来是用来判断能否接收拖拽事件的,命名canAcceptDragEvent或许更合适
if (mDragEvent == null || rig.acceptDragEvent(mContext, mDragEvent)) {
mAcceptableResolveInfos.add(rig);
}
}
notifyDataSetChanged();//更新视图
}
}
分享图标条目视图控件类的处理:
class ResolveInfoGroup{
public boolean acceptDragEvent(Context context, DragEvent event) {
//获取拖拽事件的MIME类型,这是MIMETYPE_TEXT_PLAIN,纯文本
if (ClipDescription.MIMETYPE_TEXT_PLAIN.equals(mimeType)) {//纯文本类型
//构建查询用的intent
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
sharingIntent.setType("text/plain");
sharingIntent.setPackage(getPackageName());
List<ResolveInfo> infos = context.getPackageManager().queryIntentActivities(sharingIntent, 0);
if (infos != null) {
for (ComponentName name : mNames) {
for (ResolveInfo ri2 : infos) {
//如果在pm中查询到符合intent过滤出来的组件,且与本图标条目的组件名称name相同,
//则认为该图标是可分享目标
if (name.equals(new ComponentName(ri2.activityInfo.packageName, ri2.activityInfo.name))) {
return true;
}
}
}
}
} else {//其它MIME类型
Intent intent = new Intent();
if (event.getClipDescription().getMimeTypeCount() > 1) {
intent.setAction(Intent.ACTION_SEND_MULTIPLE);
} else {
intent.setAction(Intent.ACTION_SEND);
}
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setType(mimeType);
intent.setPackage(getPackageName());
List<ResolveInfo> infos = context.getPackageManager().queryIntentActivities(intent, 0);
if (infos != null) {
for (ComponentName name : mNames) {
for (ResolveInfo ri2 : infos) {
//如果在pm中查询到符合intent过滤出来的组件,且与本图标条目的组件名称name相同,
//则认为该图标是可分享目标
if (name.equals(new ComponentName(ri2.activityInfo.packageName, ri2.activityInfo.name))) {
return true;
}
}
}
}
}
return false;
}
}
}
一键拖拽分享,侧边栏出来的图标,就是这么搜出来的,过滤出有在Manifest.xml里声明ACTION_SEND和text/plain或其它MIME类型的组件。
所以你的应用想在拖拽时,出现在onestep的侧边栏里,就应该对能接收分享的Activity,加上:
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
当然,除此之外,在onestep里的设置里,要选中该分享组件。
继续拖拽事件的派发
//前面SideView.dispatchDragEvent()最后会调用super.dispatchDragEvent(),
//即ViewGroup.dispatchDragEvent(),让其执行默认派发逻辑,最终会遍历所有的子视图,调用其Drag事件监听器
//每个分享组件图标都在ResolveInfoListAdapter.getView()时进行了Drag事件监听器的注册
//由于每个图标视图都对ACTION_DRAG_STARTED事件返回了true,因此,此后的Drag事件都会派发到此来处理。
class ResolveInfoListAdapter{
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
... ...
//为每一个分享组件图标注册拖拽事件监听器
holder.view.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(View v, DragEvent event) {
final int action = event.getAction();
switch (action) {//按SDK文档,注册OnDragListener,对下列五个动作进行处理
case DragEvent.ACTION_DRAG_STARTED:
return true;
//有View发起了startDrag()调用,系统开始处理拖拽事件,并通知所有视图,ACTION_DRAG_STARTED,
//我们返回true,表示我们感兴趣,要处理。文档说了,ACTION_DRAG_STARTED会派发给所有注册了dragListener的视图,
//但只有此时返回true,后面的事件才会继续派发给你。
case DragEvent.ACTION_DRAG_ENTERED: //拖拽图像进入到我们的分享组件图标视图区域内了,来个动画
FloatText.getInstance(mContext).show(holder.view, holder.resolveInfoGroup.getDisplayName());//显示悬浮文字
holder.view.animate().scaleX(SCALE_SIZE).scaleY(SCALE_SIZE)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setStartDelay(0)
.setDuration(100).start();
return true;
case DragEvent.ACTION_DRAG_EXITED://用户从图标区域拖出去了,来个动画表示表示,把现场还原
FloatText.getInstance(mContext).hide();//悬浮提示文字,消失
holder.view.animate().scaleX(1.0f).scaleY(1.0f).setDuration(100).start();
return true;
case DragEvent.ACTION_DRAG_LOCATION://不需要处理,其实是给父容器DragScrollView处理,处理拖动的事情
// log.d("ACTION_DRAG_LOCATION");
return true;
case DragEvent.ACTION_DROP://用户放进来了我们View的Bounding圈圈里
// log.d("ACTION_DRAG_DROP");
holder.view.animate().scaleX(1.0f).scaleY(1.0f).setDuration(100).start();
//把拖拽事件信息丢给图标条目类去处理,最终就是启动分享组件的页面
return holder.resolveInfoGroup.handleDragEvent(mContext, event);
case DragEvent.ACTION_DRAG_ENDED:
return true;
}
return false;
}
});
return holder.view;
}
}
拖拽结束,ACTION_DRAG_ENDED

class SideView{
@Override
public boolean dispatchDragEvent(DragEvent event) {
int action = event.getAction();
switch (action) {
case DragEvent.ACTION_DRAG_ENDED:
FloatText.getInstance(mContext).end();//悬浮文字消失
boolean ret = super.dispatchDragEvent(event);//依然让ViewGroup去派发到每个分享组件图标的OnDragListener,见上ResolveInfoListAdapter中的OnDragListener
onDragEnd(event);//善后处理,见下
return ret;
}
... ...
}
private void onDragEnd(DragEvent event) {
if (mSwitchContentAnim != null) {
mSwitchContentAnim.cancel();
}
int deltaWidth = mContext.getResources().getDimensionPixelSize(R.dimen.sidebar_list_anim_padding);
boolean leftMode = (SidebarController.getInstance(mContext).getSidebarMode() == SidebarMode.MODE_LEFT);
int width = getWidth() + deltaWidth;
int outTo = leftMode ? -width : width;
mSwitchContentAnim = new AnimTimeLine();
int time = 300;
//拖拽时消失的那三个列表,现在要用动画切换回来
//对所有图标增加放大、透明度渐加进入的效果
final List<View> disappearViews = new ArrayList<View>();
disappearViews.addAll(mOngoingList.getViewList());
disappearViews.addAll(mContactList.getViewList());
disappearViews.addAll(mAppList.getViewList());
int subViewCount = disappearViews.size();
if (subViewCount > 0) {
AnimTimeLine timeLine = new AnimTimeLine();
Vector3f scaleFrom = new Vector3f(0.2f, 0.2f);
Vector3f scaleTo = new Vector3f(1, 1);
Vector3f alphaFrom = new Vector3f(0, 0, 0);
Vector3f alphaTo = new Vector3f(0, 0, 1);
for (int i = 0; i < subViewCount; i++) {
View view = disappearViews.get(i);
Anim scale = new Anim(view, Anim.SCALE, time, Anim.CUBIC_OUT, scaleFrom, scaleTo);
Anim alpha = new Anim(view, Anim.TRANSPARENT, time, Anim.CUBIC_OUT, alphaFrom, alphaTo);
timeLine.addAnim(scale);
timeLine.addAnim(alpha);
}
timeLine.setDelay(time / 4);
mSwitchContentAnim.addTimeLine(timeLine);
}
//对侧边栏的bar,mScrollViewDragged,也是承载所有拖拽时的列表的父容器
//添加移动退出的效果
Anim outAnim = new Anim(mScrollViewDragged, Anim.MOVE, time, Anim.CUBIC_OUT, new Vector3f(), new Vector3f(outTo, 0));
mSwitchContentAnim.addAnim(outAnim);
mSwitchContentAnim.setAnimListener(new AnimListener() {
@Override
public void onStart() {
//动画开始, 就显示普通模式的bar,mScrollViewNormal
//难怪,侧边栏切换时,看到的是有两条黑色底边的bar在,其中一个在退出,即mScrollViewDragged
mScrollViewNormal.setVisibility(VISIBLE);
}
@Override
public void onComplete(int type) {//动画结束
if (mSwitchContentAnim != null) {
int count = disappearViews.size();
//动画结束,所有普通模式下的图标,全部显示回来
for (int i = 0; i < count; i++) {
View view = disappearViews.get(i);
view.setAlpha(1);
view.setScaleX(1);
view.setScaleY(1);
}
//动画结束,拖拽模式下的列表控件,善后处理,见下
mOngoingListFake.onDragEnd();
mContactListFake.onDragEnd();
mShareList.onDragEnd();
//最终,显示普通模式的bar,拖拽模式的bar消失
mScrollViewNormal.setVisibility(VISIBLE);
mScrollViewDragged.setTranslationX(0);
mScrollViewDragged.setVisibility(GONE);
mScrollViewDragged.scrollTo(0, 0);
mSwitchContentAnim = null;
}
}
});
//所有图标切换动画,开始
mSwitchContentAnim.start();
}
}
列表视图的处理:
class SidebarListView{
public void onDragEnd() {
if (mAdapter != null) {
mAdapter.onDragEnd();//转给适配器处理
}
}
}
适配器的处理:
class ResolveInfoListAdapter{
@Override
public void onDragEnd() {
... ...
mDragEvent = null;
updateAcceptableResolveInfos();
}
//更新支持分享的图标列表
private void updateAcceptableResolveInfos() {
mAcceptableResolveInfos.clear();
for (ResolveInfoGroup rig : mResolveInfos) {
//mDragEvent在onDragEnd()被置null,这里还进来更新,有啥意义? 况且,分享列表即将被隐藏.
if (mDragEvent == null || rig.acceptDragEvent(mContext, mDragEvent)) {
mAcceptableResolveInfos.add(rig);
}
}
notifyDataSetChanged();//更新视图
}
}
至此,拖拽从ACTION_DRAG_STARTED开始派发,到普通模式与拖拽模式两层列表的动画切换,所有图标
动画效果切换,到用户放开手,或拖拽没放置进图标区域而离开,或成功放进图标进行分享,
到最终动画结束这一切。整个流程就都在这了?错,还少一个,拖着图标在侧边栏中移动!
即ACTION_DRAG_LOCATION事件
拖拽移动 - ACTION_DRAG_LOCATION
上述流程中,没有哪个对象对ACTION_DRAG_LOCATION事件进行处理,事实上,它被派发到比SideView的父容器
DragScrollView当中去了,看它怎么处理:

class DragScrollView{
@Override
public boolean dispatchDragEvent(DragEvent event) {
//丢给内部类ScrollController处理
boolean intercept = mScrollController.onDragEvent(event);
if (intercept) {
return true;
}
return super.dispatchDragEvent(event);
}
private class ScrollController {
public boolean onDragEvent(DragEvent event){
int action = event.getAction();
if(action == DragEvent.ACTION_DRAG_LOCATION){//专门处理ACTION_DRAG_LOCATION事件
float y = event.getY();//获取拖拽视图的Y坐标
//滚动侧边栏
if(y < mTopArea && !isScrollUp()){
setEvent(event);
setRate((mTopArea - y) * 1.0f / mTopArea);
scroll(DIRECTION_DOWN);//向下滚动
return true;
}else if(y > mView.getHeight() - mBottomArea && !isScrollBottom()){
setEvent(event);
setRate(1 - (mView.getHeight() - y) * 1.0f / mBottomArea);
scroll(DIRECTION_UP);//向上滚动
return true;
}
}
//其它事件,不做处理。
setEvent(null);
setRate(0.0f);
scroll(DIRECTION_NONE);//空处理
return false;
}
private void scroll(int direction) {
mScrollDirection = direction;
mView.removeCallbacks(mScrollRunnable);
if (mScrollDirection != DIRECTION_NONE) {
mInitDel = mView.getHeight() / NUMBS;
if (mInitDel <= 0) {
mInitDel = 1;
}
//丢个消息给主线程,呆会处理
mView.post(mScrollRunnable);
}
}
private Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
//滚动DragScrollView
mView.scrollBy(0, (int) (mInitDel * mScrollDirection * mRate));
//又来一发,不断滚动,
//直到onDragEvent中条件不成立,会removeCallbacks(mScrollRunnable),停止滚动
mView.postDelayed(mScrollRunnable, DELAY);
if (mEvent != null) {
DragScrollView.super.dispatchDragEvent(mEvent);
}
}
};
}
}