Custom Layouts في الأندرويد

إنشاء Layout مخصصة في الأندرويد

AbdulAlim Rajjoubمنذ سنة

سنشرح في هذا الدرس عن كيفية إنشاء Layout مخصص في الأندرويد.
ماذا تفيدنا الCustom Layout ؟

اذا أردت أن تضع View ما في تطبيقك وتريد أن تستخدمه في أكثر من أكتفتي على سبيل المثال , وتريد أن تكون تخصيصات هذه View ثابتة في كل Activities.
أو أن تقوم بإنشاء ميثود ما داخل هذه Layout وتضع بعد الأمور التي تريد تنفيذها داخل هذه Layout التي قد لا يتيحها لك Android Framework

سأشرح في هذا المقال عن 3 استخدامات استخدمتها شخصياً في أحد مشاريعي وقد ساعدتني بشكل كبير

  1. Custom ProgressBar مع زر لإيقاف التحميل
  2. Custom Seekbar لتغيير لون الشريط وجعله متوافق مع الأنظمة القديمة
  3. Custom Layout مع BackgroundTint متوافقة مع API <21 
  4. 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

كلمات دليلية: android custom layout
1
إعجاب
932
مشاهدات
0
مشاركة
2
متابع

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !