热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

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

这篇文章主要介绍了android仿音悦台页面播放效果实例代码,新版的音悦台APP播放页面交互非常有意思,可以把播放器往下拖动,然后在底部悬浮一个小框,还可以左右拖动。

概述

新版的音悦台 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 

触摸事件的处理,由于缩放不会影响 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自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • 本文介绍了OkHttp3的基本使用和特性,包括支持HTTP/2、连接池、GZIP压缩、缓存等功能。同时还提到了OkHttp3的适用平台和源码阅读计划。文章还介绍了OkHttp3的请求/响应API的设计和使用方式,包括阻塞式的同步请求和带回调的异步请求。 ... [详细]
  • 本文介绍了一种图片处理应用,通过固定容器来实现缩略图的功能。该方法可以实现等比例缩略、扩容填充和裁剪等操作。详细的实现步骤和代码示例在正文中给出。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 显卡驱动对游戏的影响及其提升效果的研究
    本文研究了显卡驱动对游戏体验的提升效果,通过比较新旧驱动加持下的RTX 2080Ti显卡在游戏体验上的差异。测试平台选择了i9-9900K处理器和索泰RTX 2080Ti玩家力量至尊显卡,以保证数据的准确性。研究结果表明,显卡驱动的更新确实能够带来近乎50%的性能提升,对于提升游戏体验具有重要意义。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 本文介绍了在Oracle数据库中创建序列时如何选择cache或nocache参数。cache参数可以提高序列的存取速度,但可能会导致序列丢失;nocache参数可以避免序列丢失,但在高并发访问时可能导致性能问题。文章详细解释了两者的区别和使用场景。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了Java的公式汇总及相关知识,包括定义变量的语法格式、类型转换公式、三元表达式、定义新的实例的格式、引用类型的方法以及数组静态初始化等内容。希望对读者有一定的参考价值。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • C++语言入门:数组的基本知识和应用领域
    本文介绍了C++语言的基本知识和应用领域,包括C++语言与Python语言的区别、C++语言的结构化特点、关键字和控制语句的使用、运算符的种类和表达式的灵活性、各种数据类型的运算以及指针概念的引入。同时,还探讨了C++语言在代码效率方面的优势和与汇编语言的比较。对于想要学习C++语言的初学者来说,本文提供了一个简洁而全面的入门指南。 ... [详细]
author-avatar
mobiledu2502931957
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有