酷炫的Android交互動(dòng)畫和視覺效果:高仿音悅臺(tái)播放頁面
新版的音悅臺(tái) APP 播放頁面交互非常有意思,可以把播放器往下拖動(dòng),這個(gè)頁面透明漸變,然后到底部可以左右拖動(dòng)關(guān)閉播放器,然后點(diǎn)擊視頻列表有個(gè)頁面彈出來的效果,十分炫酷,于是我自己動(dòng)手實(shí)現(xiàn)了這個(gè)交互炫酷的播放器頁面。
1.廢話不多說,直接演示實(shí)現(xiàn)效果
1.1.點(diǎn)擊某個(gè)視頻,然后手指上下拖動(dòng),播放器做尺寸比例的漸變,視頻相關(guān)信息做透明度漸變
1.2.播放器只有在底部的時(shí)候,才能左右拖動(dòng),此時(shí)播放器做透明度漸變,拖動(dòng)一定范圍可以關(guān)閉播放器;然后它只有在原始位置的一小段距離內(nèi)可以往上拖動(dòng)
1.3.點(diǎn)擊視頻列表的時(shí)候,若是上次視頻是左右拖動(dòng)關(guān)閉的話,會(huì)有個(gè)彈起播放頁面的效果;若是返回鍵和返回箭頭則無效果
2.實(shí)現(xiàn)的思路講解
毫無疑問,需要自定義一個(gè)容器,然后處理它的觸摸事件,對它的子 View 進(jìn)行不同的處理。觸摸事件的處理使用 ViewDragHelper 是再適合不過了,然后你需要實(shí)現(xiàn)容器 onMeasure 和 onLayout,由于使用了 ViewDragHelper,有些坑在代碼解析的時(shí)候就會(huì)講解。
播放頁面是用新的 Activity 還僅僅是當(dāng)前 Activity 的View的問題,由于播放器縮小到底部的時(shí)候,用戶是可以滑動(dòng)視頻列表的,所以我個(gè)人認(rèn)為就是在當(dāng)前 Activity 放置一個(gè)自定義容器即可,因此為了效率考慮你可以用 ViewStub 來懶加載處理,這里方便演示我就直接 View 的形式了。
3.代碼解析
3.1.需要的變量
3.2.初始化做 ViewDragHelper 的初始化,然后 post 拿到兩個(gè)子 View,這里強(qiáng)制規(guī)定只能有兩個(gè)子元素
3.3. ViewDragHelper 的回調(diào)需要做的事情比較多,在 mFlexView 拖動(dòng)的時(shí)候需要同時(shí)設(shè)置 mFlexView 和 mFollowView 的相應(yīng)變化效果,在 mFlexView 釋放的時(shí)候需要處理關(guān)閉或收起等效果
3.4.接下來是處理測量和定位,我們實(shí)現(xiàn)的排列效果類似 LinearLayout 垂直排列的效果,這里被 measureChildWithMargins 的 heightUse 擺了一道;onLayout 的時(shí)候在位置緩存不為空的時(shí)候直接定位是因?yàn)?ViewDragHelper 在處理觸摸事件子元素在做一些平移之類的,若是有元素更新了 UI 會(huì)導(dǎo)致重新 Layout,例如我的播放器在更新時(shí)間的 TextView 時(shí)就會(huì)如此,因此在 FlexCallback 的 onViewPositionChanged 方法記錄位置,在重新 Layout 時(shí)恢復(fù)位置即可,這個(gè)也坑了好久
3.5.觸摸事件的處理,由于縮放不會(huì)影響 mFlexView 真實(shí)寬高,ViewDragHelper 仍然會(huì)阻斷 mFlexView 的真實(shí)寬高的區(qū)域,所以這里判斷手指是否落在 mFlexView 視覺上的范圍內(nèi),在才去調(diào) ViewDragHelper 的 shouldInterceptTouchEvent 方法
3.6.在 computeScroll 中,若是 mIsClosing 為 true,即關(guān)閉的整個(gè)平移執(zhí)行完畢了,通知回調(diào)事件
3.7.容器實(shí)現(xiàn)了,接下來我們繼承 YytLayout 實(shí)現(xiàn)播放器頁面的組合控件即可,再封裝一些常用的方法,這里使用的是大名鼎鼎的 Ijkplayer 實(shí)現(xiàn)的播放器,屏蔽了 IjkVideoView 的觸摸事件自己處理了;順帶一提,為了實(shí)現(xiàn)播放器 Controller 跟隨拖動(dòng)縮放的效果,放棄了常用的 PopupWindow 實(shí)現(xiàn)的思路,IjkController 直接是添加到 IjkVideoView 中的,要不彈窗實(shí)現(xiàn)跟隨播放器太麻煩了
- /**
- * Created by Oubowu on 2016/12/27 17:32.<p>
- * 仿音悅臺(tái)播放頁面的具體實(shí)現(xiàn),組合控件的形式
- */
- public class YytPlayer extends YytLayout {
- private IjkController mIjkController;
- private IjkVideoView mIjkVideoView;
- private ImageView mIvAvatar;
- private TextView mTvName;
- private TextView mTvTime;
- private TextView mTvTitle;
- private TextView mTvDesc;
- private RecyclerView mYytRecyclerView;
- private VideoListAdapter mVideoListAdapter;
- public YytPlayer(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs);
- }
- private void init(Context context, AttributeSet attrs) {
- // 繼承YytLayout并且通過merge標(biāo)簽減少層級來實(shí)現(xiàn)組合控件
- LayoutInflater.from(context).inflate(R.layout.yyt_player, this, true);
- setOnLayoutStateListener(new OnLayoutStateListener() {
- @Override
- public void onClose() {
- setVisibility(View.INVISIBLE);
- mIjkVideoView.release(true);
- }
- });
- mIjkVideoView = (IjkVideoView) findViewById(R.id.ijk_player_view);
- final int scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
- mIjkVideoView.setOnTouchListener(new OnTouchListener() {
- float mDownX = 0;
- float mDownY = 0;
- boolean mClickCancel;
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- float x = event.getX();
- float y = event.getY();
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mDownX = x;
- mDownY = y;
- break;
- case MotionEvent.ACTION_MOVE:
- if (Math.abs(mDownX - x) > scaledTouchSlop || Math.abs(mDownY - y) > scaledTouchSlop) {
- mClickCancel = true;
- }
- break;
- case MotionEvent.ACTION_UP:
- if (!mClickCancel && Math.abs(mDownX - x) <= scaledTouchSlop && Math.abs(mDownY - y) <= scaledTouchSlop) {
- // 點(diǎn)擊事件偶爾失效,只好這里自己解決了
- if (isHorizontalDragEnable()) {
- expand();
- } else {
- mIjkVideoView.toggleMediaControlsVisibility();
- }
- }
- mClickCancel = false;
- break;
- case MotionEvent.ACTION_CANCEL:
- mClickCancel = false;
- break;
- }
- return true;
- }
- });
- 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 = new VideoListAdapter();
- mVideoListAdapter.setOnItemClickCallback(new OnItemClickCallback() {
- @Override
- public void onClick(View view, int position) {
- int pos = (Integer) view.getTag();
- VideoSummary summary = mVideoListAdapter.getData().get(pos);
- playVideo(mVideoListAdapter.getData(), summary);
- }
- });
- mYytRecyclerView = (RecyclerView) findViewById(R.id.yyt_recycler_view);
- GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false);
- mYytRecyclerView.setLayoutManager(gridLayoutManager);
- mYytRecyclerView.setNestedScrollingEnabled(false);
- mYytRecyclerView.addItemDecoration(new VideoListItemDecoration(context));
- mYytRecyclerView.setAdapter(mVideoListAdapter);
- }
- // 播放視頻
- private void playVideo(String path, String name) {
- try {
- if (mIjkController == null) {
- IjkMediaPlayer.loadLibrariesOnce(null);
- IjkMediaPlayer.native_profileBegin("libijkplayer.so");
- mIjkController = new IjkController(mIjkVideoView, name);
- mIjkController.setOnViewStateListener(new IjkController.OnViewStateListener() {
- @Override
- public void onBackPress() {
- stop();
- }
- });
- mIjkVideoView.setMediaController(mIjkController);
- mIjkVideoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
- @Override
- public void onPrepared(IMediaPlayer mp) {
- mIjkVideoView.start();
- }
- });
- mIjkVideoView.setOnErrorListener(new IMediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(IMediaPlayer mp, int what, int extra) {
- Toast.makeText(getContext(), "視頻播放出錯(cuò)了╮(╯Д╰)╭", Toast.LENGTH_SHORT).show();
- return true;
- }
- });
- } else {
- // 重新設(shè)置視頻名字
- mIjkController.setVideoName(name);
- }
- // 設(shè)置這個(gè)TextureView播放器縮放就正常了
- mIjkVideoView.setRender(IjkVideoView.RENDER_TEXTURE_VIEW);
- // 因?yàn)槊看蝧etRender都會(huì)移除view再添加,為了縮放效果這里控制器是添加到IjkVideoView中的,所以這里也要重新添加才能在IjkVideoView的最上面
- mIjkController.updateControlView();
- // 顯示加載條
- mIjkController.showProgress();
- // 播放視頻
- mIjkVideoView.setVideoURI(Uri.parse(path));
- } catch (UnsatisfiedLinkError e) {
- e.printStackTrace();
- Toast.makeText(getContext(), "你的CPU是" + Build.CPU_ABI + ",當(dāng)前播放器使用的編譯版本" + BuildConfig.FLAVOR + "不匹配!", Toast.LENGTH_LONG).show();
- }
- }
- /**
- * 顯示布局,并且播放視頻
- *
- * @param data 視頻列表,用于播放頁面下面的列表布局
- * @param summary 播放的視頻信息
- */
- public void playVideo(List<VideoSummary> data, VideoSummary summary) {
- // 拿到數(shù)據(jù),設(shè)置到播放的布局的相關(guān)信息
- Glide.with(getContext()).load(summary.mTopicImg).transform(new GlideCircleTransform(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));
- }
- // 設(shè)置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);
- }
- // 開始播放
- public void start() {
- if (mIjkVideoView != null && !mIjkVideoView.isPlaying()) {
- mIjkVideoView.start();
- }
- }
- // 暫停播放
- public void pause() {
- if (mIjkVideoView != null && mIjkVideoView.isPlaying()) {
- mIjkVideoView.pause();
- }
- }
- // 停止播放
- public void stop() {
- setVisibility(View.INVISIBLE);
- if (mIjkVideoView != null) {
- mIjkVideoView.release(true);
- }
- }
- public boolean isShowing() {
- return getVisibility() == VISIBLE;
- }
- }
4.總結(jié)
說難也不難,就是各種摳細(xì)節(jié)需要腦洞,各位不妨看到好玩的交互自己打開腦洞一下,接下來可能要實(shí)現(xiàn)下 UC 瀏覽器播放器的效果,感覺也是非常有意思。