الربط والأحداث والتنسيق في JavaFX

الرسوم المتحركة والانتقالات

18 دقيقة الدرس 9 من 12

الرسوم المتحركة والانتقالات

يأتي JavaFX مزوّدًا بمحرك رسوم متحركة متكامل يعمل على نفس مؤقّت النبضات (pulse timer) الذي يستخدمه محرك التخطيط والعرض. لا تحتاج إلى مكتبة خارجية أو خيط عمل منفصل — إذ يتولّى الإطار جدولة التنفيذ والاستيفاء (interpolation) وسلامة الخيوط تلقائيًا. يغطّي هذا الدرس كل الأدوات الرئيسية في ذلك المحرك: Timeline لتحريك القيم، وفئات Transition الجاهزة التي تستهدف خصائص بعينها، ومركّبَي الانتقالات SequentialTransition وParallelTransition، وAnimationTimer منخفض المستوى للتحكم الكامل في كل إطار.

نبضة JavaFX. يُعيد JavaFX رسم المشهد بما يصل إلى 60 إطارًا في الثانية على JavaFX Application Thread المخصّص. تتكامل كل فئات الرسوم المتحركة مع هذه النبضة تلقائيًا. لا تحتاج أبدًا إلى حلقة Thread.sleep أو javax.swing.Timer.

Timeline — المحرّك متعدد الأغراض

تُحرّك Timeline أي WritableValue (وهو ما تمثّله كل خاصية JavaFX) عبر سلسلة من نقاط التفتيش KeyFrame. تُحدّد القيمة التي يجب أن تكون عليها الخاصية عند كل لحظة زمنية، ثم يقوم المحرك بالاستيفاء السلس بينها.

import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.scene.shape.Rectangle; import javafx.util.Duration; Rectangle rect = new Rectangle(50, 50, 80, 80); // تحريك موضع X للمستطيل من قيمته الحالية إلى 400 خلال ثانيتين KeyValue kv = new KeyValue(rect.translateXProperty(), 400); KeyFrame kf = new KeyFrame(Duration.seconds(2), kv); Timeline timeline = new Timeline(kf); timeline.setCycleCount(Timeline.INDEFINITE); // تكرار لا نهاية له timeline.setAutoReverse(true); // الرجوع للخلف timeline.play();

يُغلّف KeyValue مرجعًا للخاصية وقيمةً مستهدفة. أما KeyFrame فيُغلّف واحدًا أو أكثر من كائنات KeyValue إلى جانب نقطة زمنية. يمكن لـ Timeline واحدة أن تحمل أي عدد من الإطارات المفتاحية مع أي عدد من الخصائص.

يمكنك أيضًا إرفاق EventHandler<ActionEvent> عادي بإطار مفتاحي لتشغيل استدعاء راجع عند لحظة محددة، وهو مفيد لتشغيل مؤثرات صوتية أو تحديث الحالة أو تنسيق التغييرات غير المرتبطة بالخصائص:

KeyFrame tick = new KeyFrame( Duration.millis(500), e -> System.out.println("نبضة نصف ثانية"), new KeyValue(rect.opacityProperty(), 0.2) );

فئات الانتقال الجاهزة

للمهام الأكثر شيوعًا توفّر JavaFX فئات Transition جاهزة. كل منها يستهدف خاصية أو تأثيرًا محددًا، مما يغنيك عن كتابة KeyValue يدويًا.

  • TranslateTransition — تحريك العقدة على محاور X / Y / Z.
  • ScaleTransition — تحجيم العقدة حول نقطة محورها.
  • RotateTransition — تدوير العقدة حول محور Z (أو محور مخصص).
  • FadeTransition — تحريك خاصية opacity بين قيمتين.
  • FillTransition — استيفاء لون التعبئة في Shape.
  • StrokeTransition — استيفاء لون الحدود في Shape.
  • PathTransition — تحريك العقدة على طول مسار Shape اعتباطي.

تشترك كل الفئات الفرعية في نفس الواجهة البرمجية الأساسية: setDuration() وsetCycleCount() وsetAutoReverse() وplay() وpause() وstop(). إليك مثالًا كاملًا يجمع ثلاثة انتقالات:

import javafx.animation.*; import javafx.scene.shape.Circle; import javafx.scene.paint.Color; import javafx.util.Duration; Circle circle = new Circle(40, Color.CORNFLOWERBLUE); // تحريك الدائرة 200 بكسل نحو اليمين TranslateTransition slide = new TranslateTransition(Duration.seconds(1), circle); slide.setToX(200); // تكبيرها إلى 1.5× حجمها الأصلي ScaleTransition grow = new ScaleTransition(Duration.millis(600), circle); grow.setToX(1.5); grow.setToY(1.5); // إخفاؤها تدريجيًا FadeTransition fade = new FadeTransition(Duration.millis(800), circle); fade.setToValue(0.0);

تركيب الرسوم المتحركة: Sequential و Parallel

نادرًا ما تُشغّل واجهات المستخدم الحقيقية رسمًا متحركًا واحدًا معزولًا. تُشغّل SequentialTransition قائمة من الرسوم المتحركة واحدةً تلو الأخرى؛ بينما تُشغّل ParallelTransition جميعها في آنٍ واحد. كلاهما تنفّذ واجهة Animation، لذا يمكن تداخلها بشكل اعتباطي.

// تشغيل slide ثم grow ثم fade — بالترتيب SequentialTransition seq = new SequentialTransition(slide, grow, fade); seq.setOnFinished(e -> System.out.println("اكتمل كل شيء")); seq.play(); // أو: slide وgrow معًا ثم fade بعدهما ParallelTransition parallel = new ParallelTransition(slide, grow); SequentialTransition composed = new SequentialTransition(parallel, fade); composed.play();
أعِد استخدام الانتقالات بحذر. يحتفظ كائن Transition بذاكرة تشير إلى العقدة المستهدفة. إذا استدعيت play() على انتقال يعمل بالفعل فإنه يُعيد التشغيل من البداية. أنشئ نُسخًا جديدة حين تحتاج إلى تشغيل نفس الرسوم المتحركة على عقد متعددة.

Interpolators — التحكم في الإحساس بالحركة

افتراضيًا تُستوفى الخصائص خطيًا — بسرعة ثابتة من البداية إلى النهاية مما يبدو آليًا. توفّر فئة Interpolator في JavaFX عدة منحنيات تسهيل جاهزة:

  • Interpolator.LINEAR — معدّل ثابت (الافتراضي).
  • Interpolator.EASE_IN — بداية بطيئة ثم تسارع.
  • Interpolator.EASE_OUT — بداية سريعة ثم تباطؤ عند النهاية.
  • Interpolator.EASE_BOTH — بداية بطيئة ونهاية بطيئة؛ المنحنى الأكثر طبيعية لرسوم واجهات المستخدم.
  • Interpolator.SPLINE(x1, y1, x2, y2) — منحنى Bézier مكعّب لتسهيل مخصّص كليًا.
  • Interpolator.DISCRETE — يقفز فورًا إلى القيمة المستهدفة عند حدود الإطار المفتاحي؛ مفيد لسلاسل الصور الإطارية.

مرّر Interpolator كمعامل ثالث لـ KeyValue، أو استدع setInterpolator() على فئة Transition:

KeyValue kv = new KeyValue( rect.translateXProperty(), 400, Interpolator.EASE_BOTH ); // أو على فئة Transition فرعية: TranslateTransition bounce = new TranslateTransition(Duration.seconds(0.4), btn); bounce.setToY(-20); bounce.setInterpolator(Interpolator.EASE_OUT); bounce.setAutoReverse(true); bounce.setCycleCount(2); bounce.play();

AnimationTimer — التحكم الكامل في كل إطار

حين تحتاج إلى تحديث خاصية المشهد في كل إطار — كحلقة لعبة أو محاكاة فيزيائية أو مخطط بيانات في الوقت الفعلي — فإن AnimationTimer هو الأداة المناسبة. تجاوز طريقة handle(long now) الخاصة بها: now هو الطابع الزمني الحالي بالنانوثانية، تستخدمه لحساب الوقت المنقضي بين الإطارات.

import javafx.animation.AnimationTimer; double[] x = {0}; // حاوية قابلة للتعديل لاستخدامها في lambda AnimationTimer timer = new AnimationTimer() { private long lastTime = 0; @Override public void handle(long now) { if (lastTime == 0) { lastTime = now; return; } double deltaSeconds = (now - lastTime) / 1_000_000_000.0; lastTime = now; x[0] += 150 * deltaSeconds; // 150 بكسل في الثانية if (x[0] > 600) x[0] = 0; rect.setTranslateX(x[0]); } }; timer.start(); // لاحقًا، للإيقاف: timer.stop();
AnimationTimer يعمل على كل إطار. أي عمل تقوم به داخل handle() يحجب خيط الرسم. اجعله خفيفًا: تحديث المواضع، أو استهلاك طابور محسوب مسبقًا، أو تبديل مخزن صورة معاد رسمه. لا تُجرِ عمليات I/O أو استعلامات قاعدة بيانات أو حسابات حاجبة أبدًا بداخله.

دورة حياة الرسوم المتحركة

تشترك جميع الفئات الفرعية من Animation في نفس دورة الحياة:

  • play() — البدء أو الاستئناف من الموضع الحالي.
  • playFromStart() — الرجوع إلى اللحظة 0 والتشغيل.
  • pause() — التجميد عند الموضع الحالي؛ تستأنف play() من هناك.
  • stop() — التوقف وإعادة الضبط إلى اللحظة 0.
  • jumpTo(Duration) — الانتقال إلى موضع محدد دون تغيير حالة التشغيل.
  • setRate(double) — القيمة 2.0 تُشغّل بضعف السرعة؛ -1.0 تُشغّل بالعكس.
  • setOnFinished(EventHandler) — استدعاء راجع يُطلَق عند اكتمال الدورة الأخيرة.

نمط عملي: تغذية راجعة بصرية عند ضغط الزر

رسم متحرك قصير للتحجيم والعودة عند النقر على الزر هو مثال كلاسيكي يجمع ما تعلّمته. يستخدم SequentialTransition من نسختين من ScaleTransition ويقرأ بشكل واضح في كود المتحكّم:

private void animateClick(Node node) { ScaleTransition press = new ScaleTransition(Duration.millis(80), node); press.setToX(0.92); press.setToY(0.92); press.setInterpolator(Interpolator.EASE_IN); ScaleTransition release = new ScaleTransition(Duration.millis(120), node); release.setToX(1.0); release.setToY(1.0); release.setInterpolator(Interpolator.EASE_OUT); new SequentialTransition(press, release).play(); } // ربطها بالزر: myButton.setOnAction(e -> { animateClick(myButton); handleBusinessLogic(); });

الخلاصة

يمنحك JavaFX مجموعة أدوات رسوم متحركة متعددة الطبقات: Timeline للتحكم الكامل في الخصائص الاعتباطية، وفئات Transition الجاهزة للتأثيرات الشائعة، وSequentialTransition وParallelTransition للتركيب، وAnimationTimer لحلقات اللعبة على مستوى الإطار. تحدّد منحنيات Interpolator الإحساس بالحركة. في الدرس الأخير من هذا البرنامج التعليمي ستجمع كل هذه التقنيات — الربط، والأحداث، وCSS، والرسوم المتحركة — معًا في تطبيق JavaFX تفاعلي متكامل.