android仿音悦台页面交互效果实例代码

作者:code_xzh 时间:2023-03-27 12:55:54 

概述

新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,然后在底部悬浮一个小框,还可以左右拖动,然后回弹的时候也会有相应的效果,这种交互效果在头条视频和一些专注于视频的app也是很常见的。

前几天看网友有仿这个 效果,觉得不错,现在分享出来,代码可以再优化,这里的播放器使用的是B站的ijkplayer,先上两张动图。

android仿音悦台页面交互效果实例代码

当图片到达底部后,左右拖动

android仿音悦台页面交互效果实例代码

实现的思路

首先,要是拖动视图缩小的效果,我们肯定需要自定义一个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,这个大家可以自己看源码音悦台源码

来源:http://blog.csdn.net/xiangzhihong8/article/details/54098508

标签:android,页面,播放
0
投稿

猜你喜欢

  • 浅谈java面向对象中四种权限

    2023-03-09 18:43:46
  • 关于eclipse安装spring插件报错An error occurred while collecting items to be installed...解决方案

    2023-05-27 03:34:45
  • Java泛型的使用限制实例分析

    2023-05-07 20:14:52
  • IDEA JetBrains Mono字体介绍和安装教程(详解)

    2022-01-23 17:57:51
  • java的基本数据类型及属性

    2021-08-10 19:31:49
  • Springboot mybatis plus druid多数据源解决方案 dynamic-datasource的使用详解

    2021-08-01 19:27:32
  • Android 基于agora 开发视频会议的代码

    2021-11-30 02:53:04
  • Android studio实现画板功能

    2022-08-04 21:30:39
  • 浅谈Java中各种修饰符与访问修饰符的说明

    2022-10-07 00:49:52
  • Java按时间梯度实现异步回调接口的方法

    2023-11-09 10:30:29
  • Android 后台发送邮件示例 (收集应用异常信息+Demo代码)

    2022-06-24 16:31:06
  • JavaEE中struts2实现文件上传下载功能实例解析

    2023-03-09 07:54:31
  • Android SharedPreferences四种操作模式使用详解

    2021-06-06 00:15:08
  • 浅谈Java中的n种随机数产生办法

    2023-12-22 10:36:29
  • c#中单例类与静态类的区别以及使用场景

    2021-07-21 17:06:30
  • C#遍历文件夹后上传文件夹中所有文件错误案例分析

    2022-11-03 09:28:27
  • Java8通过CompletableFuture实现异步回调

    2022-07-31 01:43:53
  • Java如何获取对象属性及对应值

    2022-03-30 07:03:05
  • SpringBoot可视化监控的具体应用

    2023-07-28 20:32:02
  • 详解Spring Boot + Mybatis 实现动态数据源

    2023-06-08 13:53:18
  • asp之家 软件编程 m.aspxhome.com