أحب الـanimations. لديها القدرة على إحداث فرق كبير في تجربة ما حين تكون سريعة وجذابة وتخدم هدفًا ما. أما حين تكون بطيئة أو قبيحة أو لا منطقية، فيمكنها -حرفيًا- أن تكون السبب في إفسادها.
كنت أحب التأثير (animation) الذي كان يعمل مباشرة بعد قفل الشاشة في نظام Android على طريقة شاشات الـCRT (من نسخة 2.3 حتى 4.4)، لكني كنت أرى أيضًا أن التأثير قد «هرم» بصريًا، وحان وقت تغييره.
منذ نسخة Lollipop، غُير التأثير الخاص بقفل الشاشة إلى تأثير تلاشي إلى السواد (fade-out)، ولم يعجبني على الإطلاق.
تأثير التلاشي
تأثير التلاشي بحد ذاته ليس سيئًا بالضرورة. يمكن لهذا التأثير أن يقوم بعمل جيد جدًا في إيصال فكرة تأجيل أو إخفاء العناصر، خصوصًا حين دمجه مع تأثيرات أخرى. لكن هناك عدة مشاكل في استخدامه كتأثير لقفل الشاشة.
هناك سبب مقنع يفسر سبب كون تأثير شاشات الـCRT مقنعًا؛ هو أنها مألوفة. هناك نسبة كبيرة لناظر للتأثير أن يكون مألوفًا له، لأنه على الأرجح شاهده من قبل في ظروف مشابهة وبعد اتخاذ نفس الإجراء: إغلاق شاشة قديمة. لكن كيف لتلاشي الصورة أن يكون تأثيرًا مقنعًا لقفل الشاشة؟
كان يمكن للأمر أن يكون أفضل إن كان التأثير يعمل فقط بعد إنتهاء مهلة عمل الشاشة (time-out). في تلك الحالة يعمل التأثير بتزامن مثالي مع التقليل المتدرج للإضاءة، والذي يصاحب عادة انتهاء مهلة الشاشة. أما تلاشي الصورة إلى ظلام بعد إغلاق الشاشة وبعد ضغط زر ميكانيكي؟ لم يبد لي الأمر منطقيًا أبدًا.
المشكلة الأخرى قد تكون شخصية أكثر، إلا أنني كنت أستخدم نسخة Lollipop على جهاز Galaxy Nexus قديم، وقد كان التأثير (أو ربما الجزء التحضيري له بالأخص1) ثقيلًا جدًا على الموارد حتى أن شاشة هاتفي كانت تتجمد لما يقرب من ثانية كاملة في كل مرة أحاول فيها إغلاق الشاشة. نتج عن هذا أيضًا تأخر واضح بين سماع التأثير الصوتي لقفل الشاشة وعن بداية عمل التأثير البصري نفسه، وقد كان ذلك يدفعني للجنون.
لتلك الأسباب قررت أن أقوم إما ببناء تأثير جديد لقفل الشاشة أو إلغاءه كليًا.
البحث عن حلول
لم أستطع إيجاد أي نظام مخصص (ROM) يعمل على هاتفي ويوفر الخيار لتغيير أو إلغاء التأثير، لذلك بدأت بالبحث عن حلول أقوم بها بنفسي.
الخيار الأول الذي خطر ببالي كان أن أقوم بتعديل التأثير في الشفرة المصدرية لنظام مخصص ما. فكرت حينها في أن الإختبار (testing) سيكون مهمة صعبة جدًا، مع الوضع بالاعتبار أنني كنت سأقوم بعمل بناء كامل للنظام (build) ثم تثبيته في كل مرة أردت تجربة شيء ما. لم أمتلك حينها تجربة سابقة مع بناء نظام مخصص، لكن هذا ما كنت أفكر به.
لذلك قمت بالبحث عن حلول أبسط، وكان ذلك حين أعدت اكتشاف Xposed: إطار العمل الذي يمكّنك من تعديل النظام داخليًا دون بناء نظام مخصص، وأعجبني سماع ذلك. في هذه النقطة كنت قد سمعت عن Xposed من قبل، لكن لم أرى استخدامًا له قبل ذلك.
قمت بتثبيت Xposed على هاتفي والبحث عن إضافة جاهزة (module) في مستودع الإضافات الخاص به. وجدت واحدة تسمح بتغيير التأثير إلى أحد التأثيرات الجاهزة، لكنها لم تسمح بإلغاء التأثير.
كان عمر الإضافة أكثر من 3 أعوام، وكانت تعاني من مشاكل في الأداء. كانت الإضافة كذلك تمتلك تأثيرًا مشابهًا لتأثير شاشات الـCRT القديم، إلا أنني عرفت حينها أنني أرغب بإلغاء التأثير كليًا كخطوة أولية. لذلك لم يكن هناك حل آخر غير بناء إضافة بنفسي.
فتحت دليل التطوير للمشروع وبدأت بقراءته. كان الدليل واضحًا ومفصلًا، حتى أنه احتوى على أمثلة تدريبية لبناء إضافتك الأولى بالتدريج. أحيي المطورين على ذلك!
بناء إضافة Xposed
الحيلة الوحيدة لبناء إضافة Xposed هي معرفة من أين تبدأ من مستوى الدالة (Java method). لذا اضطررت إلى النظر في الشفرة المصدرية للنظام لمعرفة أي دالة هي المسؤولة عن بدء التأثير. ومرة أخرى، ينقذني البحث في GitHub.
لم يمض طويلًا حتى وجدت دالة باسم animateScreenStateChange
، مع وسيط (argument) باسم performScreenOffTransition
، ولم يكن للأمر أن يكون أكثر وضوحًا.
بعد بضع إعادات تشغيل والانتقال بين Android Studio ودليل Xposed، أتممت كتابة الإضافة، ولم أصدق عيني حين رأيت النتيجة لأول مرة.
هذه هي الطريقة التي تعمل بها الإضافة:
- انتظر خدمات النظام حتى تبدأ.
- الدخول في الفصيلة (class) المسماة
DisplayPowerController
والتي تحتوي الدالة. - انتظر أي نداء للدالة
animateScreenStateChange
. - احظر الدالة من العمل.
- استخرج معاملات الدالة (parameters)، وعدل المعامل
performScreenOffTransition
ليكون دائمًا بقيمةfalse
. - أعد نداء الدالة مرة أخرى بالمعاملات المعدلة.
void handleLoadPackage(LoadPackageParam lpparam) {
// 1. انتظر خدمات النظام حتى تصبح جاهزة:
if (!lpparam.packageName.equals("android"))
return;
// (سجل جاهزية الإضافة):
.log("[InstantScreenOff]: Ready.");
XposedBridge
.findAndHookMethod(
XposedHelpers// 2. الدخول في الفصيلة `DisplayPowerController`:
.findClass("com.android.server.display.DisplayPowerController", /*...*/),
XposedHelpers// 3. الدخول في الدالة `animateScreenStateChange` الخاصة بالفصيلة:
"animateScreenStateChange", int.class, boolean.class,
new XC_MethodReplacement() {
// 4. احظر النداء الأصلي للدالة:
Object replaceHookedMethod(MethodHookParam param) {
// 5. عدل المعامل `performScreenOffTransition` ليكون دائمًا بقيمة `false`:
.args[1] = false;
param// 6. أعد نداء الدالة بالمعاملات المعدلة:
return XposedBridge.invokeOriginalMethod(param.method, /*...*/, param.args);
}
}
);
}
إنك كنت تنوي بناء إضافة فدليل مشروع Xposed على GitHub هو بداية جيدة. ابدأ من «Using the Xposed Framework API» ثم «Development tutorial».
وأيضًا، هذه معلومة تمنيت لو عرفتها مسبقًا:
لتعديل دالة ما في النظام ليست جزءا من تطبيق فيه (كما في حالتنا هذه)، يجب أن تراقب خدمات النظام في الدالة handleLoadPackage
باستخدام الحزمة المسماة android
، بدلًا من com.android.systemui
مثلًا أو أي حزمة أخرى (انظر السطر #3).
بعد هذه التجربة اقتنعت أنه لا يوجد تأثير يمكن أن يعطيني الراحة التي أجدها دون تأثيرات على الإطلاق. لذا قررت إبقاء الأمر كما هو وعدم بناء تأثير مخصص.
انتقلت إلى Marshmallow بعد هذه التجربة، ويوجد الآن نظام Nougat مخصص لـGalaxy Nexus (هذا الهاتف لا يموت)، والسبب الوحيد لعدم انتقالي له هو أن Xposed لا يدعم Nougat بعد. لم أستطع الاستغناء عن الإضافة.
جدير بالذكر أن iOS أيضًا انتقل إلى استخدام تأثير التلاشي منذ iOS 7 (قبل Lollipop حتى)، ولم يبد ذلك لي أفضل بأي طريقة.
قمت بنشر الشفرة المصدرية للإضافة على GitHub إن كنت مهتمًا (ابدأ من AnimationDisabler.java
). وإن كنت فقط تريد استخدام الإضافة، فهي متوفرة الآن في مستودع Xposed.
إذًا Xposed رائع، وكل شيء في Android قابل للتخصيص دون بناء نظام مخصص كامل.
Android بطيء جدًا في تصوير لقطات الشاشة (على الأقل على جهاز بعمر 7 سنوات)، لسبب ما. وفي حين قد تبدو تأثيرات قفل الشاشة تعمل على ما يعرض على الشاشة مباشرة، إلا أنها في الحقيقة تعمل على لقطة شاشة أخدت قبل بدأ التأثير مباشرة. يمكنك اختبار ذلك عن طريق إغلاق الشاشة أثناء القيام بحركة سريعة (كالتمرير/scrolling)، وستلاحظ أن التأثير يعمل في الواقع على صورة سابقة ثابتة لما كان يعرض على الشاشة.↩︎