酷炫的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 中的,要不弹窗实现跟随播放器太麻烦了


  1. /** 
  2.  * Created by Oubowu on 2016/12/27 17:32.<p> 
  3.  * 仿音悦台播放页面的具体实现,组合控件的形式 
  4.  */ 
  5. public class YytPlayer extends YytLayout { 
  6.     private IjkController mIjkController; 
  7.     private IjkVideoView mIjkVideoView; 
  8.     private ImageView mIvAvatar; 
  9.     private TextView mTvName; 
  10.     private TextView mTvTime; 
  11.     private TextView mTvTitle; 
  12.     private TextView mTvDesc; 
  13.     private RecyclerView mYytRecyclerView; 
  14.     private VideoListAdapter mVideoListAdapter; 
  15.     public YytPlayer(Context context, AttributeSet attrs) { 
  16.         super(context, attrs); 
  17.         init(context, attrs); 
  18.     } 
  19.     private void init(Context context, AttributeSet attrs) { 
  20.         // 继承YytLayout并且通过merge标签减少层级来实现组合控件 
  21.         LayoutInflater.from(context).inflate(R.layout.yyt_player, this, true); 
  22.         setOnLayoutStateListener(new OnLayoutStateListener() { 
  23.             @Override 
  24.             public void onClose() { 
  25.                 setVisibility(View.INVISIBLE); 
  26.                 mIjkVideoView.release(true); 
  27.             } 
  28.         }); 
  29.         mIjkVideoView = (IjkVideoView) findViewById(R.id.ijk_player_view); 
  30.         final int scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 
  31.         mIjkVideoView.setOnTouchListener(new OnTouchListener() { 
  32.             float mDownX = 0; 
  33.             float mDownY = 0; 
  34.             boolean mClickCancel; 
  35.             @Override 
  36.             public boolean onTouch(View v, MotionEvent event) { 
  37.                 float x = event.getX(); 
  38.                 float y = event.getY(); 
  39.                 switch (event.getAction()) { 
  40.                     case MotionEvent.ACTION_DOWN: 
  41.                         mDownX = x; 
  42.                         mDownY = y; 
  43.                         break; 
  44.                     case MotionEvent.ACTION_MOVE: 
  45.                         if (Math.abs(mDownX - x) > scaledTouchSlop || Math.abs(mDownY - y) > scaledTouchSlop) { 
  46.                             mClickCancel = true; 
  47.                         } 
  48.                         break; 
  49.                     case MotionEvent.ACTION_UP: 
  50.                         if (!mClickCancel && Math.abs(mDownX - x) <= scaledTouchSlop && Math.abs(mDownY - y) <= scaledTouchSlop) { 
  51.                             // 点击事件偶尔失效,只好这里自己解决了 
  52.                             if (isHorizontalDragEnable()) { 
  53.                                 expand(); 
  54.                             } else { 
  55.                                 mIjkVideoView.toggleMediaControlsVisibility(); 
  56.                             } 
  57.                         } 
  58.                         mClickCancel = false; 
  59.                         break; 
  60.                     case MotionEvent.ACTION_CANCEL: 
  61.                         mClickCancel = false; 
  62.                         break; 
  63.                 } 
  64.                 return true; 
  65.             } 
  66.         }); 
  67.         mIvAvatar = (ImageView) findViewById(R.id.iv_avatar); 
  68.         mTvName = (TextView) findViewById(R.id.tv_name); 
  69.         mTvTime = (TextView) findViewById(R.id.tv_time); 
  70.         mTvTitle = (TextView) findViewById(R.id.tv_title); 
  71.         mTvDesc = (TextView) findViewById(R.id.tv_desc); 
  72.         mVideoListAdapter = new VideoListAdapter(); 
  73.         mVideoListAdapter.setOnItemClickCallback(new OnItemClickCallback() { 
  74.             @Override 
  75.             public void onClick(View view, int position) { 
  76.                 int pos = (Integer) view.getTag(); 
  77.                 VideoSummary summary = mVideoListAdapter.getData().get(pos); 
  78.                 playVideo(mVideoListAdapter.getData(), summary); 
  79.             } 
  80.         }); 
  81.         mYytRecyclerView = (RecyclerView) findViewById(R.id.yyt_recycler_view); 
  82.         GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false); 
  83.         mYytRecyclerView.setLayoutManager(gridLayoutManager); 
  84.         mYytRecyclerView.setNestedScrollingEnabled(false); 
  85.         mYytRecyclerView.addItemDecoration(new VideoListItemDecoration(context)); 
  86.         mYytRecyclerView.setAdapter(mVideoListAdapter); 
  87.     } 
  88.     // 播放视频 
  89.     private void playVideo(String path, String name) { 
  90.         try { 
  91.             if (mIjkController == null) { 
  92.                 IjkMediaPlayer.loadLibrariesOnce(null); 
  93.                 IjkMediaPlayer.native_profileBegin("libijkplayer.so"); 
  94.                 mIjkController = new IjkController(mIjkVideoView, name); 
  95.                 mIjkController.setOnViewStateListener(new IjkController.OnViewStateListener() { 
  96.                     @Override 
  97.                     public void onBackPress() { 
  98.                         stop(); 
  99.                     } 
  100.                 }); 
  101.                 mIjkVideoView.setMediaController(mIjkController); 
  102.                 mIjkVideoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() { 
  103.                     @Override 
  104.                     public void onPrepared(IMediaPlayer mp) { 
  105.                         mIjkVideoView.start(); 
  106.                     } 
  107.                 }); 
  108.                 mIjkVideoView.setOnErrorListener(new IMediaPlayer.OnErrorListener() { 
  109.                     @Override 
  110.                     public boolean onError(IMediaPlayer mp, int what, int extra) { 
  111.                         Toast.makeText(getContext(), "视频播放出错了╮(╯Д╰)╭", Toast.LENGTH_SHORT).show(); 
  112.                         return true; 
  113.                     } 
  114.                 }); 
  115.             } else { 
  116.                 // 重新设置视频名字 
  117.                 mIjkController.setVideoName(name); 
  118.             } 
  119.             // 设置这个TextureView播放器缩放就正常了 
  120.             mIjkVideoView.setRender(IjkVideoView.RENDER_TEXTURE_VIEW); 
  121.             // 因为每次setRender都会移除view再添加,为了缩放效果这里控制器是添加到IjkVideoView中的,所以这里也要重新添加才能在IjkVideoView的最上面 
  122.             mIjkController.updateControlView(); 
  123.             // 显示加载条 
  124.             mIjkController.showProgress(); 
  125.             // 播放视频 
  126.             mIjkVideoView.setVideoURI(Uri.parse(path)); 
  127.         } catch (UnsatisfiedLinkError e) { 
  128.             e.printStackTrace(); 
  129.             Toast.makeText(getContext(), "你的CPU是" + Build.CPU_ABI + ",当前播放器使用的编译版本" + BuildConfig.FLAVOR + "不匹配!", Toast.LENGTH_LONG).show(); 
  130.         } 
  131.     } 
  132.     /** 
  133.      * 显示布局,并且播放视频 
  134.      * 
  135.      * @param data    视频列表,用于播放页面下面的列表布局 
  136.      * @param summary 播放的视频信息 
  137.      */ 
  138.     public void playVideo(List<VideoSummary> data, VideoSummary summary) { 
  139.         // 拿到数据,设置到播放的布局的相关信息 
  140.         Glide.with(getContext()).load(summary.mTopicImg).transform(new GlideCircleTransform(getContext())).into(mIvAvatar); 
  141.         mTvName.setText(summary.mTopicName); 
  142.         mTvTime.setText(summary.mPtime); 
  143.         mTvTitle.setText(Html.fromHtml(summary.mTitle)); 
  144.         if (summary.mDescription.isEmpty()) { 
  145.             mTvDesc.setText(summary.mTopicDesc); 
  146.         } else { 
  147.             mTvDesc.setText(Html.fromHtml(summary.mDescription)); 
  148.         } 
  149.         // 设置YytLayout可见,并且展开 
  150.         setVisibility(View.VISIBLE); 
  151.         expand(); 
  152.         mVideoListAdapter.setData(data); 
  153.         mVideoListAdapter.setItemWidth(mYytRecyclerView.getWidth() / 2); 
  154.         mVideoListAdapter.notifyDataSetChanged(); 
  155.         // 播放视频 
  156.         playVideo(summary.mMp4HdUrl == null ? summary.mMp4Url : summary.mMp4HdUrl, summary.mTitle); 
  157.     } 
  158.     // 开始播放 
  159.     public void start() { 
  160.         if (mIjkVideoView != null && !mIjkVideoView.isPlaying()) { 
  161.             mIjkVideoView.start(); 
  162.         } 
  163.     } 
  164.     // 暂停播放 
  165.     public void pause() { 
  166.         if (mIjkVideoView != null && mIjkVideoView.isPlaying()) { 
  167.             mIjkVideoView.pause(); 
  168.         } 
  169.     } 
  170.     // 停止播放 
  171.     public void stop() { 
  172.         setVisibility(View.INVISIBLE); 
  173.         if (mIjkVideoView != null) { 
  174.             mIjkVideoView.release(true); 
  175.         } 
  176.     } 
  177.     public boolean isShowing() { 
  178.         return getVisibility() == VISIBLE; 
  179.     } 

4.总结

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

作者:oubowu

来源:51CTO

时间: 2024-10-14 20:46:12

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

Android中使用TextView实现高仿京东淘宝各种倒计时效果_Android

今天给大家带来的是仅仅使用一个TextView实现一个高仿京东.淘宝.唯品会等各种电商APP的活动倒计时.最近公司一直加班也没来得及时间去整理,今天难得休息想把这个分享给大家,只求共同学习,以及自己后续的复习.为什么会想到使用一个TextView来实现呢?因为最近公司在做一些优化的工作,其中就有一个倒计时样式,原来开发的这个控件的同事使用了多个TextView拼接在一起的,实现的代码冗余比较大,故此项目经理就说:小宏这个就交给你来优化了,并且还要保证有一定的扩展性,当时就懵逼了.不知道从何处开始

Android中使用TextView实现高仿京东淘宝各种倒计时效果

今天给大家带来的是仅仅使用一个TextView实现一个高仿京东.淘宝.唯品会等各种电商APP的活动倒计时.最近公司一直加班也没来得及时间去整理,今天难得休息想把这个分享给大家,只求共同学习,以及自己后续的复习.为什么会想到使用一个TextView来实现呢?因为最近公司在做一些优化的工作,其中就有一个倒计时样式,原来开发的这个控件的同事使用了多个TextView拼接在一起的,实现的代码冗余比较大,故此项目经理就说:小宏这个就交给你来优化了,并且还要保证有一定的扩展性,当时就懵逼了.不知道从何处开始

超酷炫的Android碎纸机效果推荐_Android

在Android开发中,有时候可能会要用到碎纸机的效果,今天小编为大家整理好代码,一起来看看吧. 首先来看下效果图 实例代码 xml <com.ldoublem.PaperShredderlib.PaperShredderView android:layout_width="200dp" android:id="@+id/ps_delete2" android:layout_height="220dp" paper:sherderBgCol

让酷炫的文字交互效果点亮你的网页

  在网页设计中,如果网站主页色彩处理得好,能让整个网站锦上添花.而一些交互效果的巧妙使用,不仅让网页瞬间亮起来,甚至在提升用户感知度和感官体验上可以起到无可替代的作用. 我们这里所说的交互主要是指,用户通过鼠标的悬停.滑动.单击.拖动等交互方式来触发事件,与目标对象产生互动. 本系列文章关于网页文字设计,我们就先分享文字的交互效果. 文字交互过程的场景应该是这样的: 一个用户在浏览一个网页时,注意到某些文字,当他用鼠标悬停.滑过.单击.拖动(交互行为)与文字(交互对象)互动时,看到文字发生了变

JavaWeb文件上传下载实例讲解(酷炫的文件上传技术)_java

一.课程概述 在Web应用系统开发中,文件上传功能是非常常用的功能,今天来主要讲讲JavaWeb中的文件上传功能的相关技术实现,并且随着互联网技术的飞速发展,用户对网站的体验要求越来越高,在文件上传功能的技术上也出现许多创新点,例如异步上传文件,拖拽式上传,黏贴上传,上传进度监控,文件缩略图,大文件断点续传,大文件秒传等等. 本课程需要的基础知识: 了解基本的Http协议内容 基本IO流操作技术 Servlet基础知识 javascript/jQuery技术基础知识 二.文件上传的基础 对于文件

Android 高仿QQ图片选择器_Android

当做一款APP,需要选择本地图片时,首先考虑的无疑是系统相册,但是Android手机五花八门,再者手机像素的提升,大图无法返回等异常因数,导致适配机型比较困难,微信.QQ都相继的在自己的APP里集成了图片选择功能,放弃了系统提供的图片选择器,这里仿造QQ做了一个本地图片选择器,PS:之前有人说"仿"写成"防"了,今儿特意注意了下,求不错. 先上一张效果图,无图无真相啊~~~ 实现的效果大概是这样的: 1.单选:跳转到本地图片选择文件夹,选择文件夹后,进入到该文件夹下

Android 高仿微信语音聊天页面高斯模糊(毛玻璃效果)_Android

目前的应用市场上,使用毛玻璃效果的APP随处可见,比如用过微信语音聊天的人可以发现,语音聊天页面就使用了高斯模糊效果. 先看下效果图:   仔细观察上图,我们可以发现,背景图以用户头像为模板,对其进行了高斯模糊,并把它作为整个页面的背景色. 关于Android如何快速实现高斯模糊(毛玻璃效果),网上一堆相关介绍,可参考下面文章一种快速毛玻璃虚化效果实现–Android. 下面直接给出模糊化工具类(已验证可行): import android.graphics.Bitmap; /** * 快速模糊

Android 高仿微信语音聊天页面高斯模糊(毛玻璃效果)

目前的应用市场上,使用毛玻璃效果的APP随处可见,比如用过微信语音聊天的人可以发现,语音聊天页面就使用了高斯模糊效果. 先看下效果图: 仔细观察上图,我们可以发现,背景图以用户头像为模板,对其进行了高斯模糊,并把它作为整个页面的背景色. 关于Android如何快速实现高斯模糊(毛玻璃效果),网上一堆相关介绍,可参考下面文章一种快速毛玻璃虚化效果实现–Android. 下面直接给出模糊化工具类(已验证可行): import android.graphics.Bitmap; /** * 快速模糊化工

Android实现酷炫的顶部栏_Android

AppBarLayout 是继承LinerLayout实现的一个ViewGroup容器组件,它是为了Material Design设计的App Bar,支持手势滑动操作的,不过经常与Toolbar.CoordinatorLayout以及CollapsingToolbarLayout等一起配合使用,在说到AppBarLayout之前,我们先简单学习一下Toolbar.和往常一样,主要还是想总结一下我在学习过程中的一些笔记以及一些需要注意的地方. 一.Toolbar Toolbar是在 Androi