转载

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,这个页面透明渐变,然后到底部可以左右拖动关闭播放器,然后点击视频列表有个页面弹出来的效果,十分炫酷,于是我自己动手实现了这个交互炫酷的播放器页面。

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

1.废话不多说,直接演示实现效果

1.1.点击某个视频,然后手指上下拖动,播放器做尺寸比例的渐变,视频相关信息做透明度渐变

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

1.2.播放器只有在底部的时候,才能左右拖动,此时播放器做透明度渐变,拖动一定范围可以关闭播放器;然后它只有在原始位置的一小段距离内可以往上拖动

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

1.3.点击视频列表的时候,若是上次视频是左右拖动关闭的话,会有个弹起播放页面的效果;若是返回键和返回箭头则无效果

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

2.实现的思路讲解

毫无疑问,需要自定义一个容器,然后处理它的触摸事件,对它的子 View 进行不同的处理。触摸事件的处理使用 ViewDragHelper 是再适合不过了,然后你需要实现容器 onMeasure 和 onLayout,由于使用了 ViewDragHelper,有些坑在代码解析的时候就会讲解。

播放页面是用新的 Activity 还仅仅是当前 Activity 的View的问题,由于播放器缩小到底部的时候,用户是可以滑动视频列表的,所以我个人认为就是在当前 Activity 放置一个自定义容器即可,因此为了效率考虑你可以用 ViewStub 来懒加载处理,这里方便演示我就直接 View 的形式了。

3.代码解析

3.1.需要的变量

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

3.2.初始化做 ViewDragHelper 的初始化,然后 post 拿到两个子 View,这里强制规定只能有两个子元素

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

3.3. ViewDragHelper 的回调需要做的事情比较多,在 mFlexView 拖动的时候需要同时设置 mFlexView 和 mFollowView 的相应变化效果,在 mFlexView 释放的时候需要处理关闭或收起等效果

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

3.4.接下来是处理测量和定位,我们实现的排列效果类似 LinearLayout 垂直排列的效果,这里被 measureChildWithMargins 的 heightUse 摆了一道;onLayout 的时候在位置缓存不为空的时候直接定位是因为 ViewDragHelper 在处理触摸事件子元素在做一些平移之类的,若是有元素更新了 UI 会导致重新 Layout,例如我的播放器在更新时间的 TextView 时就会如此,因此在 FlexCallback 的 onViewPositionChanged 方法记录位置,在重新 Layout 时恢复位置即可,这个也坑了好久

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

3.5.触摸事件的处理,由于缩放不会影响 mFlexView 真实宽高,ViewDragHelper 仍然会阻断 mFlexView 的真实宽高的区域,所以这里判断手指是否落在 mFlexView 视觉上的范围内,在才去调 ViewDragHelper 的 shouldInterceptTouchEvent 方法

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

3.6.在 computeScroll 中,若是 mIsClosing 为 true,即关闭的整个平移执行完毕了,通知回调事件

酷炫的Android交互动画和视觉效果:高仿音悦台播放页面

3.7.容器实现了,接下来我们继承 YytLayout 实现播放器页面的组合控件即可,再封装一些常用的方法,这里使用的是大名鼎鼎的 Ijkplayer 实现的播放器,屏蔽了 IjkVideoView 的触摸事件自己处理了;顺带一提,为了实现播放器 Controller 跟随拖动缩放的效果,放弃了常用的 PopupWindow 实现的思路,IjkController 直接是添加到 IjkVideoView 中的,要不弹窗实现跟随播放器太麻烦了

/** 
 * Created by Oubowu on 2016/12/27 17:32.<p> 
 * 仿音悦台播放页面的具体实现,组合控件的形式 
 */ 
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标签减少层级来实现组合控件 
        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) { 
                            // 点击事件偶尔失效,只好这里自己解决了 
                            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(), "视频播放出错了╮(╯Д╰)╭", Toast.LENGTH_SHORT).show(); 
                        return true; 
                    } 
                }); 
            } 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 (UnsatisfiedLinkError e) { 
            e.printStackTrace(); 
            Toast.makeText(getContext(), "你的CPU是" + Build.CPU_ABI + ",当前播放器使用的编译版本" + BuildConfig.FLAVOR + "不匹配!", Toast.LENGTH_LONG).show(); 
        } 
    } 
    /** 
     * 显示布局,并且播放视频 
     * 
     * @param data    视频列表,用于播放页面下面的列表布局 
     * @param summary 播放的视频信息 
     */ 
    public void playVideo(List<VideoSummary> data, VideoSummary summary) { 
        // 拿到数据,设置到播放的布局的相关信息 
        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)); 
        } 
        // 设置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.总结

说难也不难,就是各种抠细节需要脑洞,各位不妨看到好玩的交互自己打开脑洞一下,接下来可能要实现下 UC 浏览器播放器的效果,感觉也是非常有意思。

原文  http://mobile.51cto.com/android-528203.htm
正文到此结束
Loading...