概述

新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,然后在底部悬浮一个小框,还可以左右拖动,然后回弹的时候也会有相应的效果,这种交互效果在头条视频和一些专注于视频的app也是很常见的。
前几天看网友有仿这个 效果,觉得不错,现在分享出来,代码可以再优化,这里的播放器使用的是B站的ijkplayer,先上两张动图。
当图片到达底部后,左右拖动
实现的思路
首先,要是拖动视图缩小的效果,我们肯定需要自定义一个View,而根据我们项目的场景我们这里需要两个View,一个是拖动的View,另一个是浮动上下的View(可以缩小的View),为了实现拖动,我们知道必定会用到ViewDragHelper这个类,这个类专门为了拖动而设计的。
然后,对于拖动到底部的View,我们需要实现左右拖动的效果,这个其实也是比较容易实现的,我们通过ViewDragHelper的onViewPositionChanged方法来判断当前视图的状况,就可以做View进行缩放和渐变了。
代码分析
首先我们会自定义一个容器,容器的init方法会初始化两个View:mFlexView (到底拖动的View)和mFollowView (跟随触摸缩放的View)
private void init(Context context, AttributeSet attrs) {
final float density = getResources().getDisplayMetrics().density;
final float minVel = MIN_FLING_VELOCITY * density;
ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
FlexCallback flexCallback = new FlexCallback();
mDragHelper = ViewDragHelper.create(this, 1.0f, flexCallback);
// 最小拖动速度
mDragHelper.setMinVelocity(minVel);
post(new Runnable() {
@Override
public void run() {
// 需要添加的两个子View,其中mFlexView作为拖动的响应View,mLinkView作为跟随View
mFlexView = getChildAt(0);
mFollowView = getChildAt(1);
mDragHeight = getMeasuredHeight() - mFlexView.getMeasuredHeight();
mFlexWidth = mFlexView.getMeasuredWidth();
mFlexHeight = mFlexView.getMeasuredHeight();
}
});
}
ViewDragHelper 的回调需要做的事情比较多,在 mFlexView 拖动的时候需要同时设置 mFlexView 和 mFollowView 的相应变化效果,在 mFlexView 释放的时候需要处理关闭或收起等效果。所以这里我们需要对ViewDragHelper个各种回调事件进行监听。这也是本功能最核心的:
private class FlexCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
// mFlexView来响应触摸事件
return mFlexView == child;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return Math.max(Math.min(mDragWidth, left), -mDragWidth);
}
@Override
public int getViewHorizontalDragRange(View child) {
return mDragWidth * 2;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if (!mVerticalDragEnable) {
// 不允许垂直拖动的时候是mFlexView在底部水平拖动一定距离时设置的,返回mDragHeight就不能再垂直做拖动了
return mDragHeight;
}
return Math.max(Math.min(mDragHeight, top), 0);
}
@Override
public int getViewVerticalDragRange(View child) {
return mDragHeight;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (mHorizontalDragEnable) {
// 如果水平拖动有效,首先根据拖动的速度决定关闭页面,方向根据速度正负决定
if (xvel > 1500) {
mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight);
mIsClosing = true;
} else if (xvel < -1500) {
mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight);
mIsClosing = true;
} else {
// 速度没到关闭页面的要求,根据透明度来决定关闭页面,方向根据releasedChild.getLeft()正负决定
float alpha = releasedChild.getAlpha();
if (releasedChild.getLeft() < 0 && alpha <= 0.4f) {
mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight);
mIsClosing = true;
} else if (releasedChild.getLeft() > 0 && alpha <= 0.4f) {
mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight);
mIsClosing = true;
} else {
mDragHelper.settleCapturedViewAt(0, mDragHeight);
}
}
} else {
// 根据垂直方向的速度正负决定布局的展示方式
if (yvel > 1500) {
mDragHelper.settleCapturedViewAt(0, mDragHeight);
} else if (yvel < -1500) {
mDragHelper.settleCapturedViewAt(0, 0);
} else {
// 根据releasedChild.getTop()决定布局的展示方式
if (releasedChild.getTop() <= mDragHeight / 2) {
mDragHelper.settleCapturedViewAt(0, 0);
} else {
mDragHelper.settleCapturedViewAt(0, mDragHeight);
}
}
}
invalidate();
}
@Override
public void onViewPositionChanged(final View changedView, int left, int top, int dx, int dy) {
float fraction = top * 1.0f / mDragHeight;
// mFlexView缩放的比率
mFlexScaleRatio = 1 - 0.5f * fraction;
mFlexScaleOffset = changedView.getWidth() / 20;
// 设置缩放基点
changedView.setPivotX(changedView.getWidth() - mFlexScaleOffset);
changedView.setPivotY(changedView.getHeight() - mFlexScaleOffset);
// 设置比例
changedView.setScaleX(mFlexScaleRatio);
changedView.setScaleY(mFlexScaleRatio);
// mFollowView透明度的比率
float alphaRatio = 1 - fraction;
// 设置透明度
mFollowView.setAlpha(alphaRatio);
// 根据垂直方向的dy设置top,产生跟随mFlexView的效果
mFollowView.setTop(mFollowView.getTop() + dy);
// 到底部的时候,changedView的top刚好等于mDragHeight,以此作为水平拖动的基准
mHorizontalDragEnable = top == mDragHeight;
if (mHorizontalDragEnable) {
// 如果水平拖动允许的话,由于设置缩放不会影响mFlexView的宽高(比如getWidth),所以水平拖动距离为mFlexView宽度一半
mDragWidth = (int) (changedView.getMeasuredWidth() * 0.5f);
// 设置mFlexView的透明度,这里向左右水平拖动透明度都随之变化
changedView.setAlpha(1 - Math.abs(left) * 1.0f / mDragWidth);
// 水平拖动一定距离的话,垂直拖动将被禁止
mVerticalDragEnable = left < 0 && left >= -mDragWidth * 0.05;
} else {
// 不是水平拖动的处理
changedView.setAlpha(1);
mDragWidth = 0;
mVerticalDragEnable = true;
}
if (mFlexLayoutPosition == null) {
// 创建子元素位置缓存
mFlexLayoutPosition = new ChildLayoutPosition();
mFollowLayoutPosition = new ChildLayoutPosition();
}
// 记录子元素的位置
mFlexLayoutPosition.setPosition(mFlexView.getLeft(), mFlexView.getRight(), mFlexView.getTop(), mFlexView.getBottom());
mFollowLayoutPosition.setPosition(mFollowView.getLeft(), mFollowView.getRight(), mFollowView.getTop(), mFollowView.getBottom());
// Log.e("FlexCallback", "225行-onViewPositionChanged(): 【" + mFlexView.getLeft() + ":" + mFlexView.getRight() + ":" + mFlexView.getTop() + ":" + mFlexView
// .getBottom() + "】 【" + mFollowView.getLeft() + ":" + mFollowView.getRight() + ":" + mFollowView.getTop() + ":" + mFollowView.getBottom() + "】");
}
}
接下来是处理测量和定位,我们实现的排列效果类似 LinearLayout 垂直排列的效果,这里需要对 measureChildWithMargins 的 heightUse 重新设置;onLayout 的时候在位置缓存不为空的时候直接定位是因为 ViewDragHelper 在处理触摸事件子元素在做一些平移之类的,若是有元素更新了 UI 会导致重新 Layout,因此在 FlexCallback 的 onViewPositionChanged 方法记录位置,然后在回弹的时候需要通过Layout 恢复之前的视图。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desireHeight = 0;
int desireWidth = 0;
int tmpHeight = 0;
if (getChildCount() != 2) {
throw new IllegalArgumentException("只允许容器添加两个子View!");
}
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
// 测量子元素并考虑外边距
// 参数heightUse:父容器竖直已经被占用的空间,比如被父容器的其他子 view 所占用的空间;这里我们需要的是子View垂直排列,所以需要设置这个值
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, tmpHeight);
// 获取子元素的布局参数
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 计算子元素宽度,取子控件最大宽度
desireWidth = Math.max(desireWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
// 计算子元素高度
tmpHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
desireHeight += tmpHeight;
}
// 考虑父容器内边距
desireWidth += getPaddingLeft() + getPaddingRight();
desireHeight += getPaddingTop() + getPaddingBottom();
// 尝试比较建议最小值和期望值的大小并取大值
desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
}
// 设置最终测量值
setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mFlexLayoutPosition != null) {
// 因为在用到ViewDragHelper处理布局交互的时候,若是有子View的UI更新导致重新Layout的话,需要我们自己处理ViewDragHelper拖动时子View的位置,否则会导致位置错误
// Log.e("YytLayout1", "292行-onLayout(): " + "自己处理布局位置");
mFlexView.layout(mFlexLayoutPosition.getLeft(), mFlexLayoutPosition.getTop(), mFlexLayoutPosition.getRight(), mFlexLayoutPosition.getBottom());
mFollowView.layout(mFollowLayoutPosition.getLeft(), mFollowLayoutPosition.getTop(), mFollowLayoutPosition.getRight(), mFollowLayoutPosition.getBottom());
return;
}
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
int multiHeight = 0;
int count = getChildCount();
if (count != 2) {
throw new IllegalArgumentException("此容器的子元素个数必须为2!");
}
for (int i = 0; i < count; i++) {
// 遍历子元素并对其进行定位布局
final View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int left = paddingLeft + lp.leftMargin;
int right = child.getMeasuredWidth() + left;
int top = (i == 0 ? paddingTop : 0) + lp.topMargin + multiHeight;
int bottom = child.getMeasuredHeight() + top;
child.layout(left, top, right, bottom);
multiHeight += (child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
}
触摸事件的处理,由于缩放不会影响 mFlexView 真实宽高,ViewDragHelper 仍然会阻断 mFlexView 的真实宽高的区域,所以这里判断手指是否落在 mFlexView 视觉上的范围内,在才去调 ViewDragHelper 的 shouldInterceptTouchEvent 方法。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Log.e("YytLayout", mFlexView.getLeft() + ";" + mFlexView.getTop() + " --- " + ev.getX() + ":" + ev.getY());
// 由于缩放不会影响mFlexView真实宽高,这里手动计算视觉上的范围
float left = mFlexView.getLeft() + mFlexWidth * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);
float top = mFlexView.getTop() + mFlexHeight * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);
// 这里所做的是判断手指是否落在mFlexView视觉上的范围内
mInFlexViewTouchRange = ev.getX() >= left && ev.getY() >= top;
if (mInFlexViewTouchRange) {
return mDragHelper.shouldInterceptTouchEvent(ev);
} else {
return super.onInterceptTouchEvent(ev);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mInFlexViewTouchRange) {
// 这里还要做判断是因为,即使我不阻断事件,但是此Layout的子View不消费的话,事件还是给回此Layout
mDragHelper.processTouchEvent(event);
return true;
} else {
// 不在mFlexView触摸范围内,并且子View没有消费,返回false,把事件传递回去
return false;
}
}
同时我们需要对滚动事件进行监听,我们需要在此关闭的整个平移执行事件。
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
invalidate();
} else if (mIsClosing && mOnLayoutStateListener != null) {
// 正在关闭的情况下,并且拖动结束后,告知将要关闭页面
mOnLayoutStateListener.onClose();
mIsClosing = false;
}
}
/**
* 监听布局是否水平拖动关闭了
*/
public interface OnLayoutStateListener {
void onClose();
}
public void setOnLayoutStateListener(OnLayoutStateListener onLayoutStateListener) {
mOnLayoutStateListener = onLayoutStateListener;
}
/**
* 展开布局
*/
public void expand() {
mDragHelper.smoothSlideViewTo(mFlexView, 0, 0);
invalidate();
}
而在实际的应用中要实现回弹后详情页面的效果,我们需要自己实现一个组合View,这个大家可以自己看源码音悦台源码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
# android
# 仿音悦台页面
# 仿音悦台页面播放
# android开发之调用手机的摄像头使用MediaRecorder录像并播放
# Android实现歌曲播放时歌词同步显示具体思路
# Android提高之MediaPlayer播放网络音频的实现方法
# Android实现图片循环播放的实例方法
# Android使用VideoView播放本地视频和网络视频的方法
# 教你轻松制作Android音乐播放器
# Android提高之MediaPlayer音视频播放
# android使用videoview播放视频
# Android自定义播放器控件VideoView
# Android编程实现WebView全屏播放的方法(附源码)
# Android编程开发音乐播放器实例
# 拖动
# 的是
# 是因为
# 播放器
# 落在
# 自定义
# 回调
# 我不
# 也会
# 还可以
# 在此
# 就不
# 遍历
# 而在
# 对其
# 要做
# 将被
# 所做
# 两张
# 前几天
相关文章:
如何通过虚拟主机快速完成网站搭建?
c++怎么使用类型萃取type_traits_c++ 模板元编程类型判断【方法】
魔毅自助建站系统:模板定制与SEO优化一键生成指南
东莞专业网站制作公司有哪些,东莞招聘网站哪个好?
岳西云建站教程与模板下载_一站式快速建站系统操作指南
深圳企业网站制作设计,在深圳如何网上全流程注册公司?
移民网站制作流程,怎么看加拿大移民官网?
香港服务器建站指南:免备案优势与SEO优化技巧全解析
高端建站如何打造兼具美学与转化的品牌官网?
建站主机选购指南:核心配置优化与品牌推荐方案
单页制作网站有哪些,朋友给我发了一个单页网站,我应该怎么修改才能把他变成自己的呢,请求高手指点迷津?
上海网站制作网页,上海本地的生活网站有哪些?最好包括生活的各个方面的?
济南网站建设制作公司,室内设计网站一般都有哪些功能?
专业网站制作服务公司,有哪些网站可以免费发布招聘信息?
相亲简历制作网站推荐大全,新相亲大会主持人小萍萍资料?
如何高效配置香港服务器实现快速建站?
如何在IIS7上新建站点并设置安全权限?
怎么用手机制作网站链接,dw怎么把手机适应页面变成网页?
一键网站制作软件,义乌购一件代发流程?
贸易公司网站制作流程,出口贸易网站设计怎么做?
公众号网站制作网页,微信公众号怎么制作?
怀化网站制作公司,怀化新生儿上户网上办理流程?
网站视频制作书签怎么做,ie浏览器怎么将网站固定在书签工具栏?
建站10G流量真的够用吗?如何应对访问高峰?
企业网站制作公司网页,推荐几家专业的天津网站制作公司?
全景视频制作网站有哪些,全景图怎么做成网页?
,网站推广常用方法?
在线制作视频网站免费,都有哪些好的动漫网站?
详解免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)
建站之星云端配置指南:模板选择与SEO优化一键生成
如何快速搭建高效WAP手机网站吸引移动用户?
如何选择高效可靠的多用户建站源码资源?
电商网站制作价格怎么算,网上拍卖流程以及规则?
建站org新手必看:2024最新搭建流程与模板选择技巧
Thinkphp 中 distinct 的用法解析
如何在Ubuntu系统下快速搭建WordPress个人网站?
如何在西部数码注册域名并快速搭建网站?
,购物网站怎么盈利呢?
家具网站制作软件,家具厂怎么跑业务?
建站之星会员如何解锁更多建站功能?
制作宣传网站的软件,小红书可以宣传网站吗?
如何通过wdcp面板快速创建网站?
家庭建站与云服务器建站,如何选择更优?
专业公司网站制作公司,用什么语言做企业网站比较好?
如何通过老薛主机一键快速建站?
如何选择网络建站服务器?高效建站必看指南
Swift中循环语句中的转移语句 break 和 continue
建站之星安装失败:服务器环境不兼容?
宿州网站制作公司兴策,安徽省低保查询网站?
Swift中switch语句区间和元组模式匹配
*请认真填写需求信息,我们会在24小时内与您取得联系。