酷炫的Android交互动画和视觉效果:高仿音悦台播放页面
新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,这个页面透明渐变,然后到底部可以左右拖动关闭播放器,然后点击视频列表有个页面弹出来的效果,十分炫酷,于是我自己动手实现了这个交互炫酷的播放器页面。 1.废话不多说,直接演示实现效果 1.1.点击某个视频,然后手指上下拖动,播放器做尺寸比例的渐变,视频相关信息做透明度渐变 1.2.播放器只有在底部的时候,才能左右拖动,此时播放器做透明度渐变,拖动一定范围可以关闭播放器;然后它只有在原始位置的一小段距离内可以往上拖动 1.3.点击视频列表的时候,若是上次视频是左右拖动关闭的话,会有个弹起播放页面的效果;若是返回键和返回箭头则无效果 2.实现的思路讲解 毫无疑问,需要自定义一个容器,然后处理它的触摸事件,对它的子 View 进行不同的处理。触摸事件的处理使用 ViewDragHelper 是再适合不过了,然后你需要实现容器 onMeasure 和 onLayout,由于使用了 ViewDragHelper,有些坑在代码解析的时候就会讲解。 播放页面是用新的 Activity 还仅仅是当前 Activity 的View的问题,由于播放器缩小到底部的时候,用户是可以滑动视频列表的,所以我个人认为就是在当前 Activity 放置一个自定义容器即可,因此为了效率考虑你可以用 ViewStub 来懒加载处理,这里方便演示我就直接 View 的形式了。 3.代码解析 3.1.需要的变量 3.2.初始化做 ViewDragHelper 的初始化,然后 post 拿到两个子 View,这里强制规定只能有两个子元素 3.3. ViewDragHelper 的回调需要做的事情比较多,在 mFlexView 拖动的时候需要同时设置 mFlexView 和 mFollowView 的相应变化效果,在 mFlexView 释放的时候需要处理关闭或收起等效果 3.4.接下来是处理测量和定位,我们实现的排列效果类似 LinearLayout 垂直排列的效果,这里被 measureChildWithMargins 的 heightUse 摆了一道;onLayout 的时候在位置缓存不为空的时候直接定位是因为 ViewDragHelper 在处理触摸事件子元素在做一些平移之类的,若是有元素更新了 UI 会导致重新 Layout,例如我的播放器在更新时间的 TextView 时就会如此,因此在 FlexCallback 的 onViewPositionChanged 方法记录位置,在重新 Layout 时恢复位置即可,这个也坑了好久 3.5.触摸事件的处理,由于缩放不会影响 mFlexView 真实宽高,ViewDragHelper 仍然会阻断 mFlexView 的真实宽高的区域,所以这里判断手指是否落在 mFlexView 视觉上的范围内,在才去调 ViewDragHelper 的 shouldInterceptTouchEvent 方法 3.6.在 computeScroll 中,若是 mIsClosing 为 true,即关闭的整个平移执行完毕了,通知回调事件 3.7.容器实现了,接下来我们继承 YytLayout 实现播放器页面的组合控件即可,再封装一些常用的方法,这里使用的是大名鼎鼎的 Ijkplayer 实现的播放器,屏蔽了 IjkVideoView 的触摸事件自己处理了;顺带一提,为了实现播放器 Controller 跟随拖动缩放的效果,放弃了常用的 PopupWindow 实现的思路,IjkController 直接是添加到 IjkVideoView 中的,要不弹窗实现跟随播放器太麻烦了 /** *CreatedbyOubowuon2016/12/2717:32.<p> *仿音悦台播放页面的具体实现,组合控件的形式 */ publicclassYytPlayerextendsYytLayout{ privateIjkControllermIjkController; privateIjkVideoViewmIjkVideoView; privateImageViewmIvAvatar; privateTextViewmTvName; privateTextViewmTvTime; privateTextViewmTvTitle; privateTextViewmTvDesc; privateRecyclerViewmYytRecyclerView; privateVideoListAdaptermVideoListAdapter; publicYytPlayer(Contextcontext,AttributeSetattrs){ super(context,attrs); init(context,attrs); } privatevoidinit(Contextcontext,AttributeSetattrs){ //继承YytLayout并且通过merge标签减少层级来实现组合控件 LayoutInflater.from(context).inflate(R.layout.yyt_player,this,true); setOnLayoutStateListener(newOnLayoutStateListener(){ @Override publicvoidonClose(){ setVisibility(View.INVISIBLE); mIjkVideoView.release(true); } }); mIjkVideoView=(IjkVideoView)findViewById(R.id.ijk_player_view); finalintscaledTouchSlop=ViewConfiguration.get(getContext()).getScaledTouchSlop(); mIjkVideoView.setOnTouchListener(newOnTouchListener(){ floatmDownX=0; floatmDownY=0; booleanmClickCancel; @Override publicbooleanonTouch(Viewv,MotionEventevent){ floatx=event.getX(); floaty=event.getY(); switch(event.getAction()){ caseMotionEvent.ACTION_DOWN: mDownX=x; mDownY=y; break; caseMotionEvent.ACTION_MOVE: if(Math.abs(mDownX-x)>scaledTouchSlop||Math.abs(mDownY-y)>scaledTouchSlop){ mClickCancel=true; } break; caseMotionEvent.ACTION_UP: if(!mClickCancel&&Math.abs(mDownX-x)<=scaledTouchSlop&&Math.abs(mDownY-y)<=scaledTouchSlop){ //点击事件偶尔失效,只好这里自己解决了 if(isHorizontalDragEnable()){ expand(); }else{ mIjkVideoView.toggleMediaControlsVisibility(); } } mClickCancel=false; break; caseMotionEvent.ACTION_CANCEL: mClickCancel=false; break; } returntrue; } }); mIvAvatar=(ImageView)findViewById(R.id.iv_avatar); mTvName=(TextView)findViewById(R.id.tv_name); mTvTime=(TextView)findViewById(R.id.tv_time); mTvTitle=(TextView)findViewById(R.id.tv_title); mTvDesc=(TextView)findViewById(R.id.tv_desc); mVideoListAdapter=newVideoListAdapter(); mVideoListAdapter.setOnItemClickCallback(newOnItemClickCallback(){ @Override publicvoidonClick(Viewview,intposition){ intpos=(Integer)view.getTag(); VideoSummarysummary=mVideoListAdapter.getData().get(pos); playVideo(mVideoListAdapter.getData(),summary); } }); mYytRecyclerView=(RecyclerView)findViewById(R.id.yyt_recycler_view); GridLayoutManagergridLayoutManager=newGridLayoutManager(context,2,LinearLayoutManager.VERTICAL,false); mYytRecyclerView.setLayoutManager(gridLayoutManager); mYytRecyclerView.setNestedScrollingEnabled(false); mYytRecyclerView.addItemDecoration(newVideoListItemDecoration(context)); mYytRecyclerView.setAdapter(mVideoListAdapter); } //播放视频 privatevoidplayVideo(Stringpath,Stringname){ try{ if(mIjkController==null){ IjkMediaPlayer.loadLibrariesOnce(null); IjkMediaPlayer.native_profileBegin("libijkplayer.so"); mIjkController=newIjkController(mIjkVideoView,name); mIjkController.setOnViewStateListener(newIjkController.OnViewStateListener(){ @Override publicvoidonBackPress(){ stop(); } }); mIjkVideoView.setMediaController(mIjkController); mIjkVideoView.setOnPreparedListener(newIMediaPlayer.OnPreparedListener(){ @Override publicvoidonPrepared(IMediaPlayermp){ mIjkVideoView.start(); } }); mIjkVideoView.setOnErrorListener(newIMediaPlayer.OnErrorListener(){ @Override publicbooleanonError(IMediaPlayermp,intwhat,intextra){ Toast.makeText(getContext(),"视频播放出错了╮(╯Д╰)╭",Toast.LENGTH_SHORT).show(); returntrue; } }); }else{ //重新设置视频名字 mIjkController.setVideoName(name); } //设置这个TextureView播放器缩放就正常了 mIjkVideoView.setRender(IjkVideoView.RENDER_TEXTURE_VIEW); //因为每次setRender都会移除view再添加,为了缩放效果这里控制器是添加到IjkVideoView中的,所以这里也要重新添加才能在IjkVideoView的最上面 mIjkController.updateControlView(); //显示加载条 mIjkController.showProgress(); //播放视频 mIjkVideoView.setVideoURI(Uri.parse(path)); }catch(UnsatisfiedLinkErrore){ e.printStackTrace(); Toast.makeText(getContext(),"你的CPU是"+Build.CPU_ABI+",当前播放器使用的编译版本"+BuildConfig.FLAVOR+"不匹配!",Toast.LENGTH_LONG).show(); } } /** *显示布局,并且播放视频 * *@paramdata视频列表,用于播放页面下面的列表布局 *@paramsummary播放的视频信息 */ publicvoidplayVideo(List<VideoSummary>data,VideoSummarysummary){ //拿到数据,设置到播放的布局的相关信息 Glide.with(getContext()).load(summary.mTopicImg).transform(newGlideCircleTransform(getContext())).into(mIvAvatar); mTvName.setText(summary.mTopicName); mTvTime.setText(summary.mPtime); mTvTitle.setText(Html.fromHtml(summary.mTitle)); if(summary.mDescription.isEmpty()){ mTvDesc.setText(summary.mTopicDesc); }else{ mTvDesc.setText(Html.fromHtml(summary.mDescription)); } //设置YytLayout可见,并且展开 setVisibility(View.VISIBLE); expand(); mVideoListAdapter.setData(data); mVideoListAdapter.setItemWidth(mYytRecyclerView.getWidth()/2); mVideoListAdapter.notifyDataSetChanged(); //播放视频 playVideo(summary.mMp4HdUrl==null?summary.mMp4Url:summary.mMp4HdUrl,summary.mTitle); } //开始播放 publicvoidstart(){ if(mIjkVideoView!=null&&!mIjkVideoView.isPlaying()){ mIjkVideoView.start(); } } //暂停播放 publicvoidpause(){ if(mIjkVideoView!=null&&mIjkVideoView.isPlaying()){ mIjkVideoView.pause(); } } //停止播放 publicvoidstop(){ setVisibility(View.INVISIBLE); if(mIjkVideoView!=null){ mIjkVideoView.release(true); } } publicbooleanisShowing(){ returngetVisibility()==VISIBLE; } } 4.总结 说难也不难,就是各种抠细节需要脑洞,各位不妨看到好玩的交互自己打开脑洞一下,接下来可能要实现下 UC 浏览器播放器的效果,感觉也是非常有意思。 作者:oubowu 来源:51CTO