我们如何制作Android Gallery库以查看媒体内容



哈Ha! 不久前,为了寻求冒险,新项目和技术,我 成为机器人 定居在Redmadrobot。 我得到了一把椅子,一台显示器和一台Macbook,还有一个用于热身的小内部项目。 有必要完成并发布一个自写的库,以查看我们在项目中使用的媒体内容。 在本文中,我将告诉您如何在一周内了解触摸事件,如何成为开源,在Android SDK中发现错误并发布库。


开始


我们的商店应用程序的重要功能之一是能够查看附近和各个侧面的商品和服务的视频和照片。 我们不想重新发明轮子,而是去寻找一个适合我们的成品图书馆。


我们计划找到一种解决方案,以便用户可以:


  • 查看照片;
  • 使用捏缩放和双击缩放照片;
  • 观看影片
  • 翻转媒体内容;
  • 垂直滑动即可关闭照片卡(滑动即可关闭)。

这是我们发现的内容:


  • FrescoImageViewer-支持查看和滚动照片和基本手势,但不支持查看视频,仅用于Fresco库。
  • PhotoView-支持查看照片,除了滚动,滑动以关闭之外,大多数主要控制手势均不支持查看视频。
  • PhotoDraweeView-功能类似于PhotoView,但适用于Fresco。

由于没有一个库完全满足要求,因此我们不得不编写自己的库。


我们意识到图书馆


为了获得必要的功能,我们最终确定了其他库中的现有解决方案。 他们决定给发生的事情起一个小小的名字Android Gallery


我们实现功能


查看和缩放照片
为了查看照片,我们采用了PhotoView库,该库支持开箱即用的缩放。


观看影片
为了观看视频,他们使用了ExoPlayer ,它已在MediaPagerAdapter中重用。 用户首次打开视频时,将创建ExoPlayer。 切换到另一个元素时,该元素已排队,因此在下次启动视频时,将使用已经创建的ExoPlayer实例。 这使元素之间的过渡更平滑。


滚动媒体内容
在这里,我们使用了来自FrescoImageViewer的MultiTouchViewPager,它不会拦截多点触摸事件,因此我们可以向其添加手势以缩放图像。


滑动即可关闭
PhotoView不支持滑动以消除和反跳(在按比例放大或缩小图像时恢复原始图像尺寸)。
这就是我们设法处理它的方式。


学习触摸事件以实现轻击以消除


在继续支持滑动以关闭之前,您需要了解触摸事件的工作原理。 当用户触摸屏幕时,将在当前Activity中调用dispatchTouchEvent(motionEvent: MotionEvent)方法,其中MotionEvent.ACTION_DOWN进入。 此方法决定事件的命运。 您可以将motionEvent传递给onTouchEvent(motionEvent: MotionEvent)进行触摸处理,也可以在View层次结构中将其从上到下进行进一步处理。 对ACTION_UP之前的事件和/或后续事件感兴趣的View返回true。


之后,当前手势的所有事件都将落入此View,直到该手势以ACTION_UP事件结束或父ViewGroup取得控制权(然后ACTION_CANCELED事件将进入View)。 如果事件onTouchEvent(motionEvent: MotionEvent)整个View层次结构,并且没有人感兴趣,它将返回onTouchEvent(motionEvent: MotionEvent)的Activity。



在我们的Android Gallery库中,第一个ACTION_DOWN事件到达PhotoView中的dispatchTouchEvent() ,其中motionEvent传递给onTouch()实现,该实现返回true。 此外,所有事件都经过相同的链,直到下列其中一项:


  • ACTION_UP ;
  • ViewPager将尝试拦截该事件以进行滚动;
  • VerticalDragLayout将尝试拦截该事件以使滑动消失。

onInterceptTouchEvent(motionEvent: MotionEvent)方法中的仅ViewGroup可以拦截事件。 即使View对任何MotionEvent感兴趣,事件本身也将通过整个先前ViewGroup链的dispatchTouchEvent(motionEvent: MotionEvent) 。 因此,父母总是“看着”他们的孩子。 任何父ViewGroup都可以拦截事件并在onInterceptTouchEvent(motionEvent: MotionEvent)返回true,然后所有子MotionEvent.ACTION_CANCEL都将在onTouchEvent(motionEvent: MotionEvent)接收onTouchEvent(motionEvent: MotionEvent)


示例:用户将手指放在RecyclerView中的某个元素上,然后在同一元素中处理事件。 但是,只要他开始上下移动手指,RecyclerView就会拦截事件,并且滚动将开始,并且View将接收到ACTION_CANCEL事件。



在Android Gallery中,VerticalDragLayout可以拦截事件以使其滑动以关闭或关闭ViewPager以进行滚动。 但是,View可以通过调用requestDisallowInterceptTouchEvent(true)方法来阻止父级requestDisallowInterceptTouchEvent(true)事件。 如果View需要执行其操作不希望被父级拦截的操作,则这可能是必要的。


例如,当播放器中的用户跳过曲目到特定时间时。 如果父级ViewPager截获了水平滚动,则将过渡到下一个轨道。


为了处理轻扫以消除问题,我们编写了VerticalDragLayout,但是它没有从PhotoView接收触摸事件。 为了理解为什么会发生这种情况,我不得不弄清楚如何在PhotoView中处理触摸事件。


加工订单:


  1. 当VerticalDragLayout中的MotionEvent.ACTION_DOWN触发时, interceptTouchEvent()返回false,因为 该ViewGroup仅对垂直ACTION_MOVE感兴趣。 ACTION_MOVE方向在dispatchTouchEvent()定义,然后将事件传递到ViewGroup中的super.dispatchTouchEvent()方法,在该事件中将事件传递到VerticalDragLayout中的interceptTouchEvent()实现。



  2. ACTION_DOWN事件到达PhotoView中的onTouch()方法时,该视图将onTouch()拦截事件管理的功能。 所有后续手势事件都不会落入interceptTouchEvent()方法中。 仅当手势完成或在图像的左右边界上ACTION_MOVE水平ACTION_MOVE ,才可以将控制权拦截给父级。
     if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f) || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } 



    由于PhotoView仅允许父级在水平ACTION_MOVE情况下截获控件,并且要ACTION_MOVE是垂直ACTION_MOVE ,因此VerticalDragLayout无法拦截事件控制以实现手势。 要解决此问题,您需要添加在垂直ACTION_MOVE情况下拦截控制的ACTION_MOVE

     if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) || mVerticalScrollEdge == VERTICAL_EDGE_BOTH || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } 

    现在,在第一个垂直ACTION_MOVE的情况下,PhotoView将提供拦截父级的机会:



    以下ACTION_MOVE将在VerticalDragLyout中被拦截,并且ACTION_CANCEL事件将进入子视图:



    所有其他ACTION_MOVE将沿着标准链飞行到VerticalDragLayout。 重要的是,在ViewGroup从子View接管事件的控制之后,子View无法重新获得控制。



    因此,我们实现了滑动以取消对PhotoView库的支持。 在我们的库中,我们使用了在单独的模块中取出的经过修改的PhotoView源,并在原始PhotoView存储库中创建了合并请求

    我们在PhotoView中实现绑定


    回想一下,当图像缩放超出其限制时,防抖动是一种可接受比例的动画恢复。



    PhotoView中没有这种可能性。 但是,既然我们开始研究别人的开源,为什么要停在那里? 在PhotoView中,您可以设置缩放限制。 最初,这是最小值-x1,最大值-x3。 图像不能超出这些限制。



     @Override public void onScale(float scaleFactor, float focusX, float focusY) { if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) { if (mScaleChangeListener != null) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); } mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); //      checkAndDisplayMatrix(); } } 

    首先,我们决定删除“达到最小比例时禁止缩放”的条件:我们简单地抛出条件getScale() > mMinScale || scaleFactor > 1f getScale() > mMinScale || scaleFactor > 1f 。 然后突然...



    德邦赚了! 显然,发生这种情况的原因是该库的创建者决定对其进行两次安全播放,这既导致了反跳,又限制了缩放。 在onTouch事件的实现中,即在MotionEvent.ACTION_UP的情况下,如果用户缩放的比例大于/小于最大值/最小值, 则会启动AnimatedZoomRunnable ,这会将图像恢复为其原始大小。


     @Override public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; switch (ev.getAction()) { case MotionEvent.ACTION_UP: // If the user has zoomed less than min scale, zoom back // to min scale if (getScale() < mMinScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY())); handled = true; } } else if (getScale() > mMaxScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, rect.centerX(), rect.centerY())); handled = true; } } break; } } 

    除了轻扫以关闭之外,我们还在库的源代码中完成了PhotoView的确定,并创建了一个拉请求 ,其中对原始PhotoView附加了“反跳”。


    修复PhotoView中的突然错误


    PhotoView有一个非常讨厌的错误。 当用户想要通过双击放大图像时, 他患有癫痫病 图像开始缩放,可以垂直翻转180度。 即使在来自Google Play的流行应用程序中,例如在Cyan中,也可以找到此错误。



    经过长时间的搜索,我们仍然定位了该错误:有时将负scaleFactor馈入输入矩阵以进行图像缩放以进行缩放,从而导致图像翻转。


    CustomGestureDetector


     @Override public boolean onScale(ScaleGestureDetector detector) { //  -   scaleFactor < 0 //      float scaleFactor = detector.getScaleFactor(); if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) return false; //    scaleFactor   callback //      mListener.onScale(scaleFactor, detector.getFocusX(), detector.getFocusY()); return true; } 

    要从Android ScaleGestureDetector进行缩放, 我们得到scaleFactor,其计算如下:


     public float getScaleFactor() { if (inAnchoredScaleMode()) { // Drag is moving up; the further away from the gesture // start, the smaller the span should be, the closer, // the larger the span, and therefore the larger the scale final boolean scaleUp = (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) || (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan)); final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR); return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff); } return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; } 

    如果使用调试日志覆盖此方法,则可以跟踪在哪个特定变量值上获得负scaleFactor:


     mEventBeforeOrAboveStartingGestureEvent is true; SCALE_FACTOR is 0.5; mCurrSpan: 1075.4398; mPrevSpan 38.867798; scaleUp: false; spanDiff: 13.334586; eval result is -12.334586 

    怀疑他们试图通过将spanDiff乘以SCALE_FACTOR == 0.5来解决此问题。 但是,如果mCurrSpan和mPrevSpan之差超过三倍,则此解决方案将无济于事。 已经为该错误启动了票证 ,但尚未修复。
    拐杖 解决此问题的最简单方法是直接跳过负scaleFactor值。 在实践中,用户不会注意到图像有时放大得比平时稍差。


    而不是结论


    拉取请求的命运


    我们进行了本地修复,并在PhotoView中创建了最后一个“ 拉取请求” 。 尽管有些PR已经挂了一年,但我们的PR已添加到master分支中,甚至发布了新版本的PhotoView。 之后,我们决定从Android Gallery中删除本地模块,并提取正式的PhotoView来源。 为此,我必须添加对AndroidX的支持,该支持已在版本2.1.3中添加到PhotoView中。


    在哪里可以找到图书馆


    在此处查找Android Gallery库的源代码-https: //github.com/redmadrobot-spb/android-gallery以及使用说明。 为了支持仍使用支持库的项目,我们创建了android-gallery-deprecated的单独版本。 但是要小心,因为在一年中,支持库将变成南瓜!


    接下来是什么


    现在图书馆完全适合我们,但是在开发过程中出现了新的想法。 以下是其中一些:


    • 能够以任何布局使用库,而不仅仅是单独的FragmentDialog;
    • 自定义用户界面的能力;
    • 替换Gilde和ExoPlayer的能力;
    • 使用某些东西代替ViewPager的能力。

    参考文献



    UPD


    在撰写文章时,出现了FrescoImageViewer开发人员的类似 。 他们增加了对过渡动画的支持,但到目前为止,我们仅提供视频支持。 :)

Source: https://habr.com/ru/post/zh-CN433076/


All Articles