English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

تحقيق تأثير نافذة الاهتزاز في Android باستخدام View المخصص

رأيت على الإنترنت مكونًا من نوع IOS يُدعى PendulumView، والذي يحقق تأثير حركة الرافعة. لأن خطوط التقدم الأصلية ليست جذابة جدًا، فأود أن أستخدم View مخصصًا لتحقيق هذا التأثير، ويمكن استخدامه أيضًا كخطوة تقدم لصفحات التحميل في المستقبل. 

لا نريد أن نضيع الوقت، لنبدأ بمشاهدة الصورة أولاً

 

الجوانب السوداء في الأسفل هي نتيجة لسجلات غير مقصودة أثناء التسجيل، يمكن تجاهلها. 

بما أننا نصنع View مخصصة، يجب علينا اتباع عملية معيارية، الخطوة الأولى، خصائص مخصصة 

خصائص مخصصة 

إنشاء ملف الخاصية 

في مجلد res->values لمشروع Android، قم بإنشاء ملف attrs.xml يحتوي على محتوى كما يلي:

 <?xml version="1.0" encoding="utf-8"?>
<resources>
 <declare-styleable name="PendulumView">
  <attr name="globeNum" format="integer"/>
  <attr name="globeColor" format="color"/>
  <attr name="globeRadius" format="dimension"/>
  <attr name="swingRadius" format="dimension"/>
 </declare-styleable>
</resources>

يُستخدم خاصية name في declare-styleable للاشارة إلى ملف الخصائص في الكود. عادة ما تكون الخاصية name هي اسم فئة view المخصصة الخاصة بنا، مما يجعلها واضحة.

يمكن للنظام أن يقوم بكتابة العديد من المعادلات الثابتة (مثل arrays من الأعداد الصحيحة، والمؤشرات الثابتة) لنا، مما يسهل عملنا في التطوير، مثل R.styleable.PendulumView_golbeNum في الكود أدناه، التي يتم إنشاؤها لنا تلقائيًا من قبل النظام. 

يُمثل خاصية globeNum عدد الكرات، ويُمثل خاصية globeColor لون الكرات، ويُمثل خاصية globeRadius قطر الكرات، ويُمثل خاصية swingRadius نصف قطر الارتطام 

قراءة قيم الخصائص 

قراءة قيم الخصائص في طريقة بناء view المخصصة 

يمكننا أيضًا الحصول على قيم الخصائص من AttributeSet، ولكن إذا كانت قيمة الخاصية نوع مرجع، فإننا نحصل فقط على ID، ونحتاج إلى مواصلة تحليل ID للحصول على القيمة الفعلية للخصائص، حيث يساعدنا TypedArray في إنجاز العمل المذكور أعلاه مباشرة. 

public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //استخدام TypedArray لقراءة قيم الخصائص المخصصة
    TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView);
    int count = ta.getIndexCount();
    for (int i = 0; i < count; i++) {
      int attr = ta.getIndex(i);
      switch (attr) {
        case R.styleable.PendulumView_globeNum:
          عدد اللون العالمي = ta.getInt(attr, 5);
          اختراق;
        حالة R.styleable.PendulumView_globeRadius:
          محيط اللون العالمي = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));
          اختراق;
        حالة R.styleable.PendulumView_globeColor:
          لون اللون العالمي = ta.getColor(attr, اللون الأزرق);
          اختراق;
        حالة R.styleable.PendulumView_swingRadius:
          محيط الارتطام = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));
          اختراق;
      }
    }
    ta.recycle(); // لتجنب مشاكل القراءة في المرة التالية
    رسم = جديد Paint();
    لون الرسم = اللون المحدد من اللون العالمي;
  }

اعادة كتابة طريقة OnMeasure() 

@Override
  protected void onMeasure(int قياس العرض, int قياس الارتفاع) {
    super.onMeasure(قياس العرض, قياس الارتفاع);
    نمط العرض = مساحة القياس.getMode(قياس العرض);
    حجم العرض = مساحة القياس.getSize(قياس العرض);
    نمط الارتفاع = مساحة القياس.getMode(قياس الارتفاع);
    حجم الارتفاع = مساحة القياس.getSize(قياس الارتفاع);
    //高度为小球半径+摆动半径
    int height = mGlobeRadius + mSwingRadius;
    //宽度为2*摆动半径+(小球数量-1)*小球直径
    int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
    //如果测量模式为EXACTLY,则直接使用推荐值,如不为EXACTLY(一般处理wrap_content情况),使用自己计算的宽高
    setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height);
  }

其中
 int height = mGlobeRadius + mSwingRadius;
<pre name="code" class="java">int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
用于处理测量模式为AT_MOST的情况,一般是自定义View的宽高设置为了wrap_content,此时通过小球的数量,半径,摆动的半径等计算View的宽高,如下图: 

以小球个数5为例,View的大小为下图红色矩形区域 

重写onDraw()方法 

@Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //绘制除左右两个小球外的其他小球
    for (int i = 0; i < mGlobeNum - 2; i++) {
      canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint);
    }
    if (mLeftPoint == null || mRightPoint == null) {
      //初始化最左右两小球坐标
      mLeftPoint = new Point(mSwingRadius, mSwingRadius);
      mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius);
      //فتح رسم الرسم المتغير
      startPendulumAnimation();
    }
    //يرسم الكرة الصغيرة على الجانب الأيمن والكرة الصغيرة على الجانب الأيسر
    canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint);
    canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint);
  }

وظيفة onDraw هي المفتاح لـ View المخصص، حيث يتم رسم عرض View في هذا الداخل. يبدأ الرسم أولاً بالكرة الصغيرة الأخرى غير الكبيرة على الجانب الأيسر والأيمن، ثم يتم التحقق من قيم x و y للكرة الصغيرة على الجانب الأيسر والكرة الصغيرة على الجانب الأيمن، إذا كانت الرسمة الأولى، فإن القيم تكون فارغة، ويتم تعيين قيم الكرات الصغيرة الأيمن والأيسر وتشغيل الرسم المتغير. في النهاية، يتم رسم الكرات الصغيرة الأيمن والأيسر باستخدام قيم x و y لـ mLeftPoint و mRightPoint. 

فيها، mLeftPoint و mRightPoint هما كائنات من android.graphics.Point، ويستخدمونها فقط لتخزين معلومات x و y للكرة الصغيرة على الجانب الأيسر والكرة الصغيرة على الجانب الأيمن. 

استخدام الرسم المتغير 

public void startPendulumAnimation() {
    //استخدام الرسم المتغير
    final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {
      @Override
      public Object evaluate(float fraction, Object startValue, Object endValue) {
        //المفهوم fraction يستخدم لتمثيل درجة استكمال التحريك، ونقوم بحساب القيمة الحالية للتحريك بناءً عليه
        double angle = Math.toRadians(90 * fraction);
        int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));
        int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));
        Point point = new Point(x, y);
        return point;
      }
    }, new Point(), new Point());
    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        Point point = (Point) animation.getAnimatedValue();
        //الحصول على القيمة الحالية لـ fraction
        float fraction = anim.getAnimatedFraction();
        //تحديد ما إذا كان fraction ينخفض أولاً ثم يزيد، أي ما إذا كانت في حالة استعداد للارتفاع
        //تغيير الكرة المتحركة في كل مرة قبل أن تبدأ في الارتفاع
        if (lastSlope && fraction > mLastFraction) {
          isNext = !isNext;
        }
        //من خلال تغيير باستمرار قيم x و y للكرة الصغيرة على الجانب الأيسر والكرة الصغيرة على الجانب الأيمن لتحقيق تأثير الرسم المتغير
        //يستخدم isNext لتحديد ما إذا كان يجب أن يتحرك الكرة الصغيرة على الجانب الأيسر أم الكرة الصغيرة على الجانب الأيمن
        if (isNext) {
          //عندما يتحرك الكرة الصغيرة على الجانب الأيسر، يتم وضع الكرة الصغيرة على الجانب الأيمن في وضعية البداية
          mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1);
          mRightPoint.y = mSwingRadius;
          mLeftPoint.x = mSwingRadius - point.x;
          mLeftPoint.y = mGlobeRadius + point.y;
        } else {
          //عندما يتحرك الكرة اليمنى، يتم وضع الكرة اليسرى في موقف البداية
          mLeftPoint.x = mSwingRadius;
          mRightPoint.y = mSwingRadius;
          mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x;
          mRightPoint.y = mGlobeRadius + point.y;
        }
        invalidate();
        lastSlope = fraction < mLastFraction;
        mLastFraction = fraction;
      }
    });
    //يتم تعيين عدد التكرارات على التكرار اللانهائي
    anim.setRepeatCount(ValueAnimator.INFINITE);
    //يتم تعيين نمط التكرار على عرض العكس
    anim.setRepeatMode(ValueAnimator.REVERSE);
    anim.setDuration(200);
    // تعيين interpolator للتحكم في سرعة التغيير في الرسوم المتحركة
    anim.setInterpolator(new DecelerateInterpolator());
    anim.start();
  }

 فيه يتم استخدام طريقة ValueAnimator.ofObject لتمكين التعامل مع كائن Point، مما يجعله أكثر تجسيدًا ووضوحًا. بالإضافة إلى ذلك، يتم استخدام طريقة ofObject مع كائن TypeEvaluator مخصص، مما يؤدي إلى الحصول على قيمة fraction، وهي عدد صغير يتغير بين 0 و1. لذلك، لا تعني القيمتين التاليتين startValue (new Point()) وendValue (new Point()) أي شيء، ويمكن تمريرها بشكل مباشر دون كتابتها، ولكن تم كتابتها هنا لغرض الفهم. بنفس الطريقة، يمكن استخدام طريقة ValueAnimator.ofFloat(0f, 1f) للحصول على عدد صغير يتغير بين 0 و1.

     final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {
      @Override
      public Object evaluate(float fraction, Object startValue, Object endValue) {
        //المفهوم fraction يستخدم لتمثيل درجة استكمال التحريك، ونقوم بحساب القيمة الحالية للتحريك بناءً عليه
        double angle = Math.toRadians(90 * fraction);
        int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));
        int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));
        Point point = new Point(x, y);
        return point;
      }
    }, new Point(), new Point());

من خلال fraction، نحسب تغير زاوية الارتداد، من 0 إلى 90 درجة

 

مقاس mSwingRadius-mGlobeRadius يمثل طول الخط الأخضر في الصورة، مسار الارتداد، مسار مركز الكرة هو دائرة مركزية بحجم (mSwingRadius-mGlobeRadius)، ويغير قيمة x بـ (mSwingRadius-mGlobeRadius) *sin(angle)، ويغير قيمة y بـ (mSwingRadius-mGlobeRadius) *cos(angle) 

المركز الفعلي للكرة المتناظرة يقع في (mSwingRadius-x،mGlobeRadius+y) 

مسار الكرة المتحركة على الجانب الأيمن يشبه ذلك على الجانب الأيسر، ولكن في اتجاه مختلف. يقع المركز الفعلي للكرة المتحركة على الجانب الأيمن (mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x،mGlobeRadius+y) 

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

        float fraction = anim.getAnimatedFraction();
        //تحديد ما إذا كان fraction ينخفض أولاً ثم يزيد، أي ما إذا كانت في حالة استعداد للارتفاع
        //تغيير الكرة المتحركة في كل مرة قبل أن تبدأ في الارتفاع
        if (lastSlope && fraction > mLastFraction) {
          isNext = !isNext;
        }
        //تسجيل ما إذا كان fraction السابق يقل باستمرار
        lastSlope = fraction < mLastFraction;
        //تسجيل fraction السابقة
        mLastFraction = fraction;

 هذه الأنماط تستخدم لحساب متى يجب تغيير الكرة المتحركة، وقد تم ضبط هذا الرسم البياني ليعمل بلفة دائرية، واللفة تكون عكسية، لذا ففترة الرسم البياني هي عملية رمي الكرة وإسقاطها. خلال هذه العملية، يستبدل قيمة fraction من 0 إلى 1، ومن 1 إلى 0. إذن متى يكون بداية دورة جديدة للرسم البياني؟ في لحظة رمي الكرة، وفي هذه اللحظة يمكن تغيير الكرة المتحركة لتحقيق تأثير رمي الكرة على اليسار وإسقاطها على اليمين، وإسقاط الكرة على اليمين ورميها على اليسار. 

إذن كيف يمكن التقاط هذا الوقت؟ 

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

    anim.setDuration(200);
    // تعيين interpolator للتحكم في سرعة التغيير في الرسوم المتحركة
    anim.setInterpolator(new DecelerateInterpolator());
    anim.start();

تم تعيين فترة الرسوم المتحركة إلى 200 ميليسي ثانية، يمكن للقراء تعديل هذه القيمة لتغيير سرعة الارتداد للكرة.

تم تعيين م interpolator للرسوم المتحركة، حيث أن رمي الكرة هو عملية تتباطأ تدريجيًا، وارتدادها عملية تسارع تدريجي، لذا يتم استخدام DecelerateInterpolator لتحقيق تأثير التبطء، ويتم استخدام تأثير التسارع عند تشغيل الرسوم المتحركة في العكس. 

تم إنشاء شريط التقدم المخصص لتحريك الساعة بعد بدء الرسوم المتحركة، قم بتشغيله الآن لرؤية التأثير!

هذا هو نهاية محتوى هذا المقال، نأمل أن يكون قد ساعدكم في التعلم، ونأمل أيضًا أن تدعموا دليل الشعور بالصوت.

بيان: محتوى هذا المقال تم جمعه من الإنترنت، ويتمتع المالك الأصلي بحقوق الطبع والنشر، ويتم جمع المحتوى من قبل المستخدمين على الإنترنت بشكل متعاوني وتحميله تلقائيًا، ويملك هذا الموقع حقوق الملكية، ويتم تعديل المحتوى بشكل إنساني، ولا يتحمل هذا الموقع أي مسؤولية قانونية. إذا وجدت محتوى يشتبه في انتهاك حقوق النسخ، فنرجو منك إرسال بريد إلكتروني إلى: notice#oldtoolbag.com (عند إرسال البريد الإلكتروني، يرجى استبدال '#' بـ '@') لإبلاغنا، وتقديم الدليل على ذلك، وسيتم حذف المحتوى المزعوم فورًا إذا تم التحقق من صحة الشكوى.

أنت قد تحب