欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-02-20 19:45:40
概述 新版的音悦台 app 播放页面交互非常有意思,可以把播放器往下拖动,然后在底部悬浮一个小框,还可以左右拖动,然后回弹的时候也会有相应的效果,这种交互效果在头条视...

概述

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

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