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

概述

新版的音悦台 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,这个大家可以自己看源码音悦台源码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android使用VideoView播放本地视频和网络视频的方法

    1.效果展示 2.布局文件 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="matc

  • Android实现歌曲播放时歌词同步显示具体思路

    我们需要读取以上歌词文件的每一行转换成成一个个歌词实体: 复制代码 代码如下: public class LyricObject { public int begintime; // 开始时间 public int endtime; // 结束时间 public int timeline; // 单句歌词用时 public String lrc; // 单句歌词 } 可根据当前播放器的播放进度与每句歌词的开始时间,得到当前屏幕中央高亮显示的那句歌词.在UI线程中另起线程,通过回调函数 onDra

  • Android编程开发音乐播放器实例

    本文实例讲述了Android编程开发音乐播放器,分享给大家供大家参考,具体如下: 音乐播放器中综合了以下内容: SeekBar.ListView.广播接收者(以代码的形式注册Receiver).系统服务.MediaPlayer 实现的功能: 1.暂停/播放.下一首/上一首,点击某一首时播放 2.支持拖动进度条快进 3.列表排序 4.来电话时,停止播放,挂断后继续播放 5.可在后台播放 效果图: 界面: main.xml: <?xml version="1.0" encoding=

  • Android提高之MediaPlayer播放网络音频的实现方法

    前面有文章曾经地介绍过MediaPlayer的基本用法,这里就更加深入地讲解MediaPlayer的在线播放功能.本文主要实现MediaPlayer在线播放音频的功能,由于在线视频播放比在线音频播放复杂,因此先介绍在线音频播放的实现,这样可以帮助大家逐步深入了解MediaPlayer的在线播放功能. 先来看看本文程序运行的结果如下图所示: main.xml的源码如下: <?xml version="1.0" encoding="utf-8"?> <

  • Android自定义播放器控件VideoView

    介绍 最近要使用播放器做一个简单的视频播放功能,开始学习VideoView,在横竖屏切换的时候碰到了点麻烦,不过在查阅资料后总算是解决了.在写VideoView播放视频时候定义控制的代码全写在Actvity里了,写完一看我靠代码好乱,于是就写了个自定义的播放器控件,支持指定大小,可以横竖屏切换,手动左右滑动快进快退.好了,下面开始. 效果图有点卡,我也不知道为啥..... VideoView介绍 这个是我们实现视频播放最主要的控件,详细的介绍大家百度就去看,这里介绍几个常用的方法. 用于播放视频

  • Android提高之MediaPlayer音视频播放

    前面文章已经详细介绍了Android界面的入门技术,相信大家在看完和跟着练习之后,会对于常用的Layout和View都会有一定的了解了,接下来就不再强调介绍界面了,而是针对具体的常见功能而展开. 本文将介绍MediaPlayer的使用.MediaPlayer可以播放音频和视频,另外也可以通过VideoView来播放视频,虽然VideoView比MediaPlayer简单易用,但定制性不如用MediaPlayer,这需要视情况选择.MediaPlayer播放音频比较简单,但是要播放视频就需要Sur

  • 教你轻松制作Android音乐播放器

    欣赏一下我们清爽的界面吧~ 如果是只用activity来制作这样的东西简直是太小儿科了,此处我们当然用的是service 首先我们先上service的代码: 1.如果我们要访问service的属性和方法,那么在activity肯定是以bindservice的方法实现的,而在service中的onbind方法也是必须要实现的,onbind返回的Ibinder对象在activity的serviceconnection中得到使用. 2.activity获取到Ibinder对象,可以进一步获取服务对象和

  • Android实现图片循环播放的实例方法

    很多时候,我们需要展示在客户端展示图片,而且是动态显示,即不停地自行切换图片.下面我们来看一下具体的实现方法.首先,我们需要在XML...    很多时候,我们需要展示在客户端展示图片,而且是动态显示,即不停地自行切换图片.下面我们来看一下具体的实现方法. 首先,我们需要在XML文件中配置一下将要播放图片的控件(main.xml): 复制代码 代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearL

  • android开发之调用手机的摄像头使用MediaRecorder录像并播放

    我们玩玩手机的录像功能吧.做个DEMO. 看看录制过程: 复制代码 代码如下: mediarecorder = new MediaRecorder();// 创建mediarecorder对象 // 设置录制视频源为Camera(相机) mediarecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); // 设置录制完成后视频的封装格式THREE_GPP为3gp.MPEG_4为mp4 mediarecorder.setOutputFo

  • Android编程实现WebView全屏播放的方法(附源码)

    本文实例讲述了Android编程实现WebView全屏播放的方法.分享给大家供大家参考,具体如下: 最近因为项目要用webview加载html5的视频,开始不能全屏播,做了很久才做出来!那按我的理解说下怎么实现全屏吧. 首先写布局文件activity_main.xml: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.

  • android使用videoview播放视频

    复制代码 代码如下: public class Activity01 extends Activity{ /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState); setContentView(R.layout.main); final VideoView vid

随机推荐