كيف صنعنا مكتبة Android Gallery لعرض محتوى الوسائط



مرحبا يا هبر! منذ وقت ليس ببعيد ، بحثًا عن المغامرة والمشاريع والتقنيات الجديدة ، أنا أصبح روبوت استقر في Redmadrobot. حصلت على كرسي وشاشة وماك بوك ومشروع داخلي صغير للإحماء. كان من الضروري إنهاء ونشر مكتبة مكتوبة ذاتيًا لعرض محتوى الوسائط الذي نستخدمه في مشاريعنا. سأخبرك في المقالة عن كيفية فهم أحداث اللمس في الأسبوع ، وأن تصبح مصدرًا مفتوحًا ، وتجد مشكلة في Android sdk وتنشر مكتبة.


ابدأ


تتمثل إحدى الميزات المهمة لتطبيقات متجرنا في القدرة على عرض مقاطع الفيديو والصور الخاصة بالسلع والخدمات بالقرب من جميع الجهات. لم نكن نريد إعادة اختراع العجلة وبحثنا عن مكتبة جاهزة تناسبنا.


لقد خططنا لإيجاد مثل هذا الحل حتى يتمكن المستخدم من:


  • عرض الصور
  • توسيع نطاق الصور باستخدام قرصة للتكبير والنقر المزدوج.
  • شاهد فيديو
  • التقليب محتوى الوسائط.
  • أغلق بطاقة الصورة باستخدام انتقاد رأسي (التمرير السريع للإقالة).

إليك ما وجدناه:


  • FrescoImageViewer - يدعم العرض والتمرير خلال الصور والإيماءات الأساسية ، لكنه لا يدعم عرض مقاطع الفيديو وهو مخصص لمكتبة Fresco .
  • PhotoView - يدعم عرض الصور ، معظم إيماءات التحكم الرئيسية ، باستثناء التمرير والسحب للإغلاق ، لا تدعم عرض مقاطع الفيديو.
  • PhotoDraweeView - مشابه في وظائف PhotoView ، لكن مخصص لـ Fresco.

نظرًا لأن أيا من المكتبات وجدت أنها تفي تمامًا بالمتطلبات ، كان علينا أن نكتب متطلباتنا الخاصة.


نحن ندرك المكتبة


للحصول على الوظائف اللازمة ، وضعنا اللمسات الأخيرة على الحلول الحالية من المكتبات الأخرى. قرروا إعطاء اسم متواضع Android Gallery لما حدث.


نحن ننفذ وظائف


عرض الصور وتكبيرها
لعرض الصور ، التقطنا مكتبة PhotoView ، والتي تدعم التوسع خارج الصندوق.


مشاهدة الفيديو
لمشاهدة الفيديو ، أخذوا ExoPlayer ، والذي يتم إعادة استخدامه في MediaPagerAdapter . عندما يفتح المستخدم مقطع فيديو لأول مرة ، يتم إنشاء ExoPlayer. عند التبديل إلى عنصر آخر ، يتم وضعه في قائمة الانتظار ، وبالتالي في المرة التالية التي تبدأ فيها تشغيل الفيديو ، سيتم استخدام مثيل ExoPlayer الذي تم إنشاؤه بالفعل. هذا يجعل الانتقال بين العناصر أكثر سلاسة.


التمرير محتوى الوسائط
استخدمنا هنا MultiTouchViewPager من FrescoImageViewer ، الذي لا يعترض أحداث اللمس المتعدد ، لذلك تمكنا من إضافة إيماءات إليها لتوسيع نطاق الصورة.


انتقد لإقالة
لم يدعم PhotoView التمرير السريع للرفض والانسحاب (استعادة حجم الصورة الأصلية عند تكبير الصورة لأعلى أو لأسفل).
إليك كيف تمكنا من التعامل معها.


تعلم الأحداث التي تعمل باللمس لتنفيذ انتقاد لإقالة


قبل الانتقال لدعم التمرير السريع ، يجب عليك فهم كيفية عمل أحداث اللمس. عندما يلمس المستخدم الشاشة ، يتم استدعاء أسلوب dispatchTouchEvent(motionEvent: MotionEvent) في النشاط الحالي ، حيث يحصل MotionEvent.ACTION_DOWN على. هذه الطريقة تقرر مصير الحدث. يمكنك تمرير motionEvent إلى onTouchEvent(motionEvent: MotionEvent) للمعالجة باللمس ، أو السماح لها بالانتقال إلى أبعد من ذلك ، من أعلى إلى أسفل ، في عرض التسلسل الهرمي. طريقة العرض ، المهتمة بالحدث و / أو الأحداث اللاحقة قبل ACTION_UP ، تعود إلى ACTION_UP الحقيقية.


بعد ذلك ، تقع جميع أحداث الإيماءة الحالية في طريقة العرض هذه حتى تنتهي الإيماءة بحدث ACTION_UP أو تتولى مجموعة ViewGroup الرئيسية السيطرة (ثم ACTION_CANCELED حدث ACTION_CANCELED إلى العرض). إذا استمر الحدث حول التسلسل الهرمي onTouchEvent(motionEvent: MotionEvent) بالكامل ولم يكن أي شخص مهتمًا ، onTouchEvent(motionEvent: MotionEvent) إلى النشاط في onTouchEvent(motionEvent: MotionEvent) .



في مكتبة Android Gallery الخاصة بنا ، يأتي أول حدث ACTION_DOWN إلى dispatchTouchEvent() في motionEvent ، حيث motionEvent تمرير onTouch() إلى تطبيق onTouch() الذي يعود حقيقيًا. علاوة على ذلك ، تمر جميع الأحداث بنفس السلسلة حتى واحدة من:


  • ACTION_UP ؛
  • سيحاول ViewPager اعتراض الحدث للتمرير ؛
  • سيحاول VerticalDragLayout اعتراض الحدث للتمرير السريع للإغلاق.

يمكن فقط ViewGroup في أسلوب onInterceptTouchEvent(motionEvent: MotionEvent) اعتراض الأحداث. حتى إذا كانت طريقة العرض مهتمة بأي من MotionEvent ، فسوف يمر الحدث نفسه من خلال dispatchTouchEvent(motionEvent: MotionEvent) لسلسلة ViewGroup السابقة بأكملها. وفقا لذلك ، الآباء دائما "مشاهدة" أطفالهم. يمكن لأي مجموعة ViewGroup الأصل اعتراض الحدث والعودة إلى onInterceptTouchEvent(motionEvent: MotionEvent) ، ثم ستتلقى جميع MotionEvent.ACTION_CANCEL في onTouchEvent(motionEvent: MotionEvent) .


مثال: يحمل المستخدم إصبعًا على عنصر في RecyclerView ، ثم تتم معالجة الأحداث في نفس العنصر. ولكن بمجرد أن يبدأ في تحريك إصبعه لأعلى / لأسفل ، سيعترض RecyclerView الأحداث ، وسيبدأ التمرير ، وسيستلم ACTION_CANCEL الحدث ACTION_CANCEL .



في معرض الأندرويد ، يمكن لـ VerticalDragLayout اعتراض الأحداث للتمرير السريع للإغلاق أو ViewPager للتمرير. لكن طريقة العرض يمكن أن تمنع الوالد من requestDisallowInterceptTouchEvent(true) الأحداث عن طريق استدعاء الأسلوب requestDisallowInterceptTouchEvent(true) . قد يكون هذا ضروريًا إذا كانت طريقة العرض بحاجة إلى تنفيذ إجراءات لا يكون اعتراضها من قِبل الوالد أمرًا مرغوبًا بالنسبة لنا.


على سبيل المثال ، عندما يتخطى مستخدم في لاعب مسارًا إلى وقت معين. إذا اعترض ViewPager الأصل على التمرير الأفقي ، فسيكون هناك انتقال إلى المسار التالي.


للتعامل مع التمرير السريع للإغلاق ، كتبنا VerticalDragLayout ، لكنه لم يتلق أحداث اتصال من PhotoView. لفهم سبب حدوث ذلك ، اضطررت إلى معرفة كيفية معالجة أحداث اللمس في PhotoView.


ترتيب المعالجة:


  1. عندما يكون MotionEvent.ACTION_DOWN في VerticalDragLayout ، interceptTouchEvent() تشغيل interceptTouchEvent() ، مما يؤدي إلى إرجاع false ، لأن تهتم ViewGroup هذه فقط بـ ACTION_MOVE العمودي. يتم تعريف اتجاه ACTION_MOVE في dispatchTouchEvent() ، وبعد ذلك يتم تمرير الحدث إلى الأسلوب super.dispatchTouchEvent() في ViewGroup ، حيث يتم تمرير الحدث إلى تطبيق interceptTouchEvent() في VerticalDragLayout.



  2. عندما يصل الحدث ACTION_DOWN إلى طريقة onTouch() في onTouch() ، يسلب العرض القدرة على اعتراض إدارة الأحداث. لا تقع جميع أحداث الإيماءات اللاحقة في طريقة interceptTouchEvent() . يتم منح القدرة على اعتراض التحكم للوالد فقط في حالة اكتمال الإيماءة أو في حالة 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 العمودي.

     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 الفرصة لاعتراض الوالد:



    سيتم اعتراض ACTION_MOVE التالي في VerticalDragLyout ، وسيتم ACTION_CANCEL حدث ACTION_CANCEL إلى ACTION_CANCEL :



    ACTION_MOVE جميع ACTION_MOVE الأخرى إلى VerticalDragLayout على طول السلسلة القياسية. من المهم أنه بعد سيطرة ViewGroup على الأحداث من عرض الطفل ، لا يمكن للطفل مشاهدة استعادة السيطرة.



    لذلك قمنا بتنفيذ التمرير السريع لرفض دعم مكتبة PhotoView. في مكتبتنا ، استخدمنا مصادر PhotoView المعدلة المأخوذة في وحدة نمطية منفصلة ، وقمنا بإنشاء طلب دمج في مستودع PhotoView الأصلي.

    نحن نطبق debinds في PhotoView


    تذكر أن debounce هو استعادة للرسوم المتحركة بمقياس مقبول عندما يتم تغيير حجم الصورة إلى خارج حدودها.



    لم يكن هناك مثل هذا الاحتمال في 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 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.



    بعد بحث طويل ، ما زلنا نترجم هذا الخطأ: في بعض الأحيان يتم تغذية مقياس سلبيFactor بمصفوفة الإدخال لتحجيم الصورة للتحجيم ، مما يؤدي إلى انعكاس الصورة.


    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. على الرغم من حقيقة أن بعض PRS كانت معلقة هناك لمدة عام ، تم إضافة PRS لدينا إلى الفرع الرئيسي وحتى تم إصدار نسخة جديدة من PhotoView. بعد ذلك قررنا استبعاد الوحدة المحلية من Android Gallery وسحب مصادر PhotoView الرسمية. للقيام بذلك ، اضطررت إلى إضافة دعم لنظام AndroidX ، والذي تمت إضافته إلى PhotoView في الإصدار 2.1.3 .


    أين تجد المكتبة


    ابحث عن الكود المصدري لمكتبة Android Gallery هنا - https://github.com/redmadrobot-spb/android-gallery ، بالإضافة إلى تعليمات الاستخدام. ولدعم المشروعات التي لا تزال تستخدم مكتبة الدعم ، أنشأنا إصدارًا منفصلًا من معرض android-deprecated . ولكن كن حذرًا ، لأنه في غضون عام ستتحول مكتبة الدعم إلى قرع!


    ما التالي


    الآن المكتبة تناسبنا تمامًا ، ولكن في عملية التطوير نشأت أفكار جديدة. هؤلاء بعض منهم:


    • القدرة على استخدام المكتبة في أي تخطيط ، وليس فقط FragmentDialog منفصل ؛
    • القدرة على تخصيص واجهة المستخدم.
    • القدرة على استبدال Gilde و ExoPlayer ؛
    • القدرة على استخدام شيء ما بدلاً من ViewPager.

    المراجع



    محدث


    أثناء كتابة مقال ، خرجت مكتبة مماثلة من مطوري FrescoImageViewer . لقد أضافوا دعمًا للرسوم المتحركة التي تمر بمرحلة انتقالية ، ولكن حتى الآن لا يتوفر لدينا سوى دعم الفيديو. :)

Source: https://habr.com/ru/post/ar433076/


All Articles