سنشرح في هذا الدرس عن كيفية إنشاء Layout مخصص في الأندرويد.
ماذا تفيدنا الCustom Layout ؟
اذا أردت أن تضع View ما في تطبيقك وتريد أن تستخدمه في أكثر من أكتفتي على سبيل المثال , وتريد أن تكون تخصيصات هذه View ثابتة في كل Activities.
أو أن تقوم بإنشاء ميثود ما داخل هذه Layout وتضع بعد الأمور التي تريد تنفيذها داخل هذه Layout التي قد لا يتيحها لك Android Framework
سأشرح في هذا المقال عن 3 استخدامات استخدمتها شخصياً في أحد مشاريعي وقد ساعدتني بشكل كبير
- Custom ProgressBar مع زر لإيقاف التحميل
- Custom Seekbar لتغيير لون الشريط وجعله متوافق مع الأنظمة القديمة
- Custom Layout مع BackgroundTint متوافقة مع API <21
- Custom View مع أنيميشن بسيط لإخفاء أو إظهار View (يمكنك رؤية المكتبة على Github)
سأتحدث عن كل استخدام بتفصيل أكبر ولماذا استخدمته...
Custom ProgressBar
قمت بصنع ProgressBar ووضعت داخله ImageButton عبارة عن زر "X" ,فعندما يكون لديك عملية رفع او تحميل للملفات فإنه يظهر الProgressBar وداخله زر الإلغاء ,وعند الضغط على هذه الLayout أو الزر سيتم إيقاف عملية الرفع أو التحميل.
قد يقول البعض أنه يمكنك فعل هذا عن طريق وضع FrameLayout وداخله PorgressBar و زر في XML ثم تعريفهم في الجافا والخ.. , ولكن في حالتي استخدمت هذه Custom Layout بأكثر من 8 ملفات XML!,فإذا أردت استخدام هذه الطريقة التقليدية فيجب علي نسخ نفس الLayout مرارً وتكراراً ناهيك عن تعريفها في Java والخ..
لهذا سنقوم بإنشاء FrameLayout مخصصة ونضع داخلها ProgressBar و زر الإلعاء "X"
نبدأ بإنشاء كلاس سنسميه ProgressBarWithCancel ونجعله extends FrameLayout ,ستجد أن Android Studio يظهر لك خط أحمر وأنه يجب عليك عمل إنشاء Constuctors الخاصة ب FrameLayout
وهذه Constructors يجب عليك إنشاءها عندما تقوم بعمل extends لأي View.
نقوم بإنشاءها بهذا الشكل
public class ProgressBarWithCancel extends FrameLayout {
public ProgressBarWithCancel(@NonNull Context context) {
super(context);
}
public ProgressBarWithCancel(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ProgressBarWithCancel(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
نلاحظ أنه يتم عمل 3 Constructors يمكنك عدم إنشاء الرابعة ) .
الأولى تستدعى اذا قمت باستدعاء هذه Layout برمجياً بدون استخدام XML بهذا الشكل
ProgressBarWithCancel frameLayout = new ProgressBarWithCancel(this);
الثانية تستدعى عند استخدامها في XML
اما بالنسبة للثالثة (ألق نظرة على هذا الرابط)
الآن سننشئ ميثود نسميها init وهي التي تستدعى عندما يتم إنشاء Layout ,وداخل هذه الميثود سنقوم بوضع ال ProgressBar و ImageButton
public class ProgressBarWithCancel extends FrameLayout {
public ProgressBarWithCancel(@NonNull Context context) {
super(context);
init(context);
}
public ProgressBarWithCancel(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ProgressBarWithCancel(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
}
}
نبدأ بتعريف ProgressBar
ثم نقوم بتعريف LayoutParams وهي التي تحدد حجم ومكان View وجعلنا الطول والعرض WRAP_CONTENT و Gravity Center ثم وضعنا هذه الparams ل progressBar
private void init(Context context) {
ProgressBar progressBar = new ProgressBar(context);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
progressBar.setLayoutParams(params);
}
ونفس الأمر نطبقه على ImageButton بالإضافة الى ازالة Background ووضع أيقونة الزر
ولاحظ أننا استخدمنا نفس params لأننا نريد الزر أيضاً أن يكون WRAP_CONTENT وأن يكون في المنتصف
ProgressBar progressBar = new ProgressBar(context);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
progressBar.setLayoutParams(params);
ImageButton imageButton = new ImageButton(context);
imageButton.setLayoutParams(params);
imageButton.setBackground(null);
imageButton.setImageResource(R.drawable.ic_clear);
الآن نذهب الى activity_main.xml ونقوم بإضافة هذه الView بهذا الشكل
كما نرى فيكون اسم الLayout عبارة عن اسم Package + اسم الكلاس(يمكنك فقط كتابة اسم الكلاس و Android Studio سيظهر كامل الإسم)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.devlomi.customlayouts.ProgressBarWithCancel
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
الآن اذا جربت تشغيل التطبيق فإنك لن ترى شيئاً ,وكأنك لم تضع أي شيئ :D
وذلك لأنه قمنا فقط بتعريف هذه Views ولم نقم بإضافتها الى FrameLayout
نعود الى الكلاس ونقوم بإضافة الViews عبر ميثود addView
private void init(Context context) {
ProgressBar progressBar = new ProgressBar(context);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
progressBar.setLayoutParams(params);
ImageButton imageButton = new ImageButton(context);
imageButton.setLayoutParams(params);
imageButton.setBackground(null);
imageButton.setImageResource(R.drawable.ic_clear);
addView(progressBar);
addView(imageButton);
}
نعيد تشغيل التطبيق وسنجده بهذا الشكل
لكن ماذا اذا أردنا أن نقوم بعمل setProgress بشكل برمجي؟
سنقوم أولاً بتعريف ProgressBar ك Global ,ثم نقوم بإنشاء ميثود نسميها setProgress وتأخذ int الProgress الذي نريد أن نضعه ,وداخلها نقوم بعمل setProgress ل progressBar
ونفس الفكرة يمكنك تطبيقها لأي خاصة من خواص ProgressBar
private ProgressBar progressBar ;
.....................
private void init(Context context) {
progressBar = new ProgressBar(context);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
progressBar.setLayoutParams(params);
ImageButton imageButton = new ImageButton(context);
imageButton.setLayoutParams(params);
imageButton.setBackground(null);
imageButton.setImageResource(R.drawable.ic_clear);
addView(progressBar);
addView(imageButton);
}
public void setProgress(int progress) {
progressBar.setProgress(progress);
}
ويمكنك استخدامها في الأكتفتي بهذا الشكل
ProgressBarWithCancel progressBarWithCancel = findViewById(R.id.progress_with_cancel);
progressBarWithCancel.setProgress(progress);
Custom Seekbar
أردت تغيير لون الشريط في SeekBar في تطبيقي بدون أن أقوم بعمل Custom Drawable وتخصيص الكثير من الأشياء ,أريد فقط تغيير اللون مع إمكانية تخصيصه
نقوم بتنفيذ نفس الفكرة ولكن هذه المرة نقوم بعمل extends Seekbar
وكما نرى فإنه تم توليد ال3 Constructors
public class DevlomiSeekbar extends AppCompatSeekBar {
public DevlomiSeekbar(Context context) {
super(context);
}
public DevlomiSeekbar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DevlomiSeekbar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
سنقوم أيضاً بإنشاء نفس الميثود init ولكن سنضيف بارامتر جديد وهو
AttributeSet attrs
سنستخدم هذه Attributes لتخصيص اللون من خلال XML
اذا قمت باستخدام بعض المكتبات مثل CircleImageView او CardView او FloatingActionButton فستجد أنه يمكنك تخصيص بعض الأمور من XML عبر الأمر
app:attrName="value"
سنقوم بفعل نفس الشيئ
ولهذا سنقوم بإنشاء ملف XML جديد يحتوي على اسماء الأشياء التي نريد تخصيصها ,في حالتنا اللون
نذهب الى مجلد values ونقوم بإنشاء ملف XML جديد نسميه attrs.xml ,يجب أن يكون بنفس الإسم
عند انشاءه سنجده بهذا الشكل
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
داخل Resources نقوم بوضع styleable ,ستجد أن الAndroid Studio يقترح عليك أسماء Custom Layouts لديك في الكلاس
الstyleable عبارة عن Array تحتوي على اسماء الصفات التي سنضعها ,علي سبيل المثال seekColor
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DevlomiSeekbar">
</declare-styleable>
</resources>
الآن سنضع الصفة التي نريد تخصيصها,سنسميها على سبيل المثال seekColor
اما بالنسبة ل format فهي نوع القيمة التي نريدها ,في حالتنا هي color.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DevlomiSeekbar">
<attr name="seekColor" format="color"/>
</declare-styleable>
</resources>
بعض انواع القيم التي يمكنك استخدامها
reference مثلأ صورة من drawable
diemension قياس ما
int,boolean,string الخ...
سنذهب الى الميثود init ونقوم بتعريف Styleable Array داخلها
وقمنا بإعطائه attrs القادمة من Constructor بالإضافة الى اسم styleable وهو "DevlomiSeekbar" والقيم الإفتراضية 0
ثم قمنا بالتحقق اذا كانت array ليست null عندها سنبدأ بتعريف seekColor ,وقمنا باستخدام الميثود getColor التي تأخذ اسم الصفة من ملف XML ,والقيمة الافتراضية في حالة لم تتم استدعاءها من XML هي -1.
وفي حالة كانت لاتساوي -1 عندها سنقوم بتغيير لون Seekbar
ونلاحظ أخيراً أنه قمنا بعمل recycle لتوفير بعض الموارد (ألقِ نظرة على هذا الرابط)
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DevlomiSeekbar, 0, 0);
if (array != null) {
int seekColor = array.getColor(R.styleable.DevlomiSeekbar_seekColor, -1);
if (seekColor != -1) {
}
array.recycle();
}
}
الآن سنبدأ بتغيير لون الشريط
قمنا بأخذ الDrawable الإفتراضية الخاصة ب Seekbar وقمنا بعمل mutate لنبدأ التعديل عليها
ثم قمنا بعمل setColorFilter لتغيير اللون ووضعنا mode SRC_IN
وأخيراً قمنا بوضع هذه الDrawable عبر setProgressDrawable
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DevlomiSeekbar, 0, 0);
if (array != null) {
int seekColor = array.getColor(R.styleable.DevlomiSeekbar_seekColor, -1);
if (seekColor != -1) {
Drawable progressDrawable = getProgressDrawable().mutate();
progressDrawable.setColorFilter(seekColor, PorterDuff.Mode.SRC_IN);
setProgressDrawable(progressDrawable);
}
array.recycle();
}
}
سنقوم الآن باستدعاء الميثود init في 3 Constructors
public class DevlomiSeekbar extends AppCompatSeekBar {
public DevlomiSeekbar(Context context) {
super(context);
init(context, null); //null because there is no attributes when this is called!
}
public DevlomiSeekbar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public DevlomiSeekbar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DevlomiSeekbar, 0, 0);
if (array != null) {
int seekColor = array.getColor(R.styleable.DevlomiSeekbar_seekColor, -1);
if (seekColor != -1) {
Drawable progressDrawable = getProgressDrawable().mutate();
progressDrawable.setColorFilter(seekColor, PorterDuff.Mode.SRC_IN);
setProgressDrawable(progressDrawable);
}
array.recycle();
}
}
ثم سنتوجه الى xml ونضع الكلاس الذي صنعناه
ونضع الصفة seekColor
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:background="#e1e1e1">
<com.devlomi.customlayouts.DevlomiSeekbar
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:seekColor="#e53935"
/>
</RelativeLayout>
ملاحظة:يجب عليك تعريف app namespace عبر
xmlns:app="http://schemas.android.com/apk/res-auto"
نجرب تشغيل التطبيق وسنجد أنه تم تغيير لون الشريط الى اللون الأحمر
ماذا إذا أردنا تغيير لون Thumb (الدائرة الصغيرة)؟
بنفس الفكرة ,نقوم بتعريف صفة جديدة نسميها thumbColor مثلاً
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DevlomiSeekbar">
<attr name="seekColor" format="color" />
<attr name="thumbColor" format="color" />
</declare-styleable>
</resources>
وفي كلاس الجافا نطبق كما فعلنا سابقاً ولكن هذه المرة على Thumb
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DevlomiSeekbar, 0, 0);
if (array != null) {
int seekColor = array.getColor(R.styleable.DevlomiSeekbar_seekColor, -1);
if (seekColor != -1) {
Drawable progressDrawable = getProgressDrawable().mutate();
progressDrawable.setColorFilter(seekColor, PorterDuff.Mode.SRC_IN);
setProgressDrawable(progressDrawable);
}
int thumbColor = array.getColor(R.styleable.DevlomiSeekbar_thumbColor, -1);
if (thumbColor != -1) {
Drawable thumbDrawable = getThumb().mutate();
thumbDrawable.setColorFilter(seekColor, PorterDuff.Mode.SRC_IN);
setThumb(thumbDrawable);
}
array.recycle();
}
}
وفي activity_main.xml نضيف thumbColor
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#e1e1e1"
tools:context=".MainActivity">
<com.devlomi.customlayouts.DevlomiSeekbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:seekColor="#e53935"
app:thumbColor="#e53935" />
</RelativeLayout>
نشغل التطبيق ونلاحظ أنه تم تغيير Thumb الى نفس اللون
Custom Layout مع BackgroundTint
قمت بإنشاء Custom Relative Layout في تطبيقي لأنه كان لدي صورة ما وفي كل Activity أريد تغيير صورة هذه Drawable .
الحل الذي يوفره Android Framework هو
android:backgroundTint="colorValue"
ولكن عيبه أن يدعم فقط API21 فما فوق,وتطبيقي يدعم minSdk17 لهذا طبقت نفس فكرة Seekbar ولكن على RelativeLayout
قمت بتعريف styleable جديد ووضعت داخله bgTintColor
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DevlomiSeekbar">
<attr name="seekColor" format="color" />
<attr name="thumbColor" format="color" />
</declare-styleable>
<declare-styleable name="RelativeLayoutWithBackgroundTint">
<attr name="bgTintColor" format="color" />
</declare-styleable>
</resources>
ونطبق نفس الفكرة في ملف الجافا
public class RelativeLayoutWithBackgroundTint extends RelativeLayout {
public RelativeLayoutWithBackgroundTint(Context context) {
super(context);
init(context, null);
}
public RelativeLayoutWithBackgroundTint(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RelativeLayoutWithBackgroundTint(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RelativeLayoutWithBackgroundTint, 0, 0);
if (array != null) {
int bgTintColor = array.getColor(R.styleable.RelativeLayoutWithBackgroundTint_bgTintColor, -1);
if (bgTintColor != -1) {
Drawable background = getBackground().mutate();
background.setColorFilter(bgTintColor, PorterDuff.Mode.SRC_IN);
}
array.recycle();
}
}
}
أخيراً في activity_main.xml
<com.devlomi.customlayouts.RelativeLayoutWithBackgroundTint
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:background="@drawable/ic_message"
app:bgTintColor="#737272" />
قد تكون هذه الإستخدامات بسيطة ,ويمكن تنفيذها بطرق أخرى, ولكنها ساعدتني كثيراً في تقليل الكود وعدم التكرار,وقد تساعدك في حالات أخرى كثيرة.
شاركنا في التعليقات كيف ولماذا استخدمت CustomLayout 😉
المشروع على Github
التعليقات (0)
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !