প্রায়ই আমাদের এমন কিছু ভিউ অ্যাপে ব্যবহার করার দরকার হয় যেটা পুরো অ্যাপের বেশ কিছু জায়গায়ই আমরা ইউজ করি। এরকম ভিউগুলো আমরা প্রতিবার xml কপি-পেস্ট করে বিভিন্ন Activity-Fragment এ বসাতে পারি। আবার চাইলে আলাদা একটা Custom View বানিয়ে ফেলতে পারি। যদি কাস্টম ভিউ বানাই তাহলে কোডটা reuse করা ও maintain করা সহজ হয়। ভিউগুলোতে ডেটা দেখানো বা অ্যাক্সেস করাও সহজ হয়। আজকের এই পোস্টে আমরা দেখব কিভাবে কাস্টম ভিউ বানিয়ে আমাদের ডেভেলপমেন্ট লাইফকে আরেকটু সহজ করা যায়।
Problem Description
উপরের ছবিতে ঢাকার সূর্যোদয় ও সূর্যাস্তের সময় দেখানো হয়েছে। প্রতিটা আইটেম দেখানোর জন্য আমরা একটা ImageView, ২ টা TextView ও divider দেখানোর জন্য একটা View widget ব্যবহার করা হয়েছে। সাধারণ ভাবে Activity তে উপরের ডিজাইন করার জন্য দুইটা আইটেমের জন্য মোট চারটা widget ব্যবহার করার দরকার হচ্ছে। এই একই activity-তে যদি এরকম চারটা আইটেম দেখাতে হয় তাহলে আমাদের দরকার হবে মোট ১৬ টা widget (চারটা ImageView, আটটা TextView ইত্যাদি)। তাহলে আমাদের Activity’র xml কোডটা অনেক বড় হয়ে যাবে।
আমাদের এই প্রবলেমটা সলভ করার জন্য টার্গেট হচ্ছে আমরা প্রতিটা ভিউ শো করার জন্য প্রতিবার চারটা করে widget ব্যবহার করব না। আমাদেরকে একটা কাস্টম widget বানাতে হবে যেটা xml এ একবার ব্যবহার করলেই একটা আইটেম শো হবে। Widget-টি নিচের মত করে ব্যবহার করতে হবে:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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.hellohasan.androidcustomview.CustomView android:id="@+id/sunrise_custom_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" app:layout_constraintTop_toBottomOf="@+id/textView" app:setImageDrawable="@drawable/sun_rise" app:setTitle="Sun rise time - Dhaka, Bangladesh" tools:setSubTitle="5:25 AM" /> </androidx.constraintlayout.widget.ConstraintLayout>
Solution
আমরা যদি Activity’র xml এ প্রতিবার চারটা করে widget এর ইউজ কমাতে চাই তাহলে একটা বুদ্ধি হচ্ছে এই চারটা widget দিয়ে একটা আলাদা layout design করে যেখানে দরকার সেখানে layout টা <include> ট্যাগের মাধ্যমে যোগ করা। এতে xml এর সাইজ কমবে। কিন্তু সমস্যা হবে একই ভিউতে একাধিকবার layout টা include করলে একই আইডির একাধিক widget হয়ে যাবে। তাই এই পদ্ধতিতে আমরা প্রবলেম সলভ করতে পারছি না। তো চলুন দেখা যাক কাস্টম ভিউ কিভাবে বানানো যায়।
আমরা যেহেতু কাস্টম ভিউ বানাচ্ছি পুরো অ্যাপে ইউজ করার জন্য তাই ভিউটা হবে জেনারেল একটা ফরমেটে। ভিউয়ের widget-গুলোকে আমরা identify করতে পারি imageView, title, sub title আর ডিভাইডার হিসাবে। যদি উপরের ছবির জন্য কাস্টম ভিউ ইউজ করতে চাই তাহলে কাস্টম ভিউয়ের টাইটেল এ শো করব “Sunrise time – Dhaka, Bangladesh” টেক্সটটি। সাবটাইটেলে দেখাব টাইম। ইমেজ ভিউতে সেট করব সূর্যোদয়ের একটা ছবি। একই ভাবে অন্য কোনো পারপাসে যখন এটা ইউজ করব সেভাবেই ডেটাগুলো সেট করব। তো প্রথমেই চলুন আলাদা একটা layout ফাইলে ভিউটা ডিজাইন করে ফেলি।
উপরের ডিজাইনটি করার জন্য xml ডিজাইনটি হতে পারে এরকম (/layout/custom_view.xml):
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"> <ImageView android:id="@+id/imageView" android:layout_width="60dp" android:layout_height="60dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="ContentDescription" tools:src="@drawable/location" /> <TextView android:id="@+id/titleTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:textSize="16sp" android:textStyle="bold" app:layout_constraintStart_toEndOf="@+id/imageView" app:layout_constraintTop_toTopOf="@+id/imageView" tools:text="This is title. It should be in one line" /> <TextView android:id="@+id/subtitleTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" app:layout_constraintStart_toStartOf="@+id/titleTextView" app:layout_constraintTop_toBottomOf="@+id/titleTextView" tools:text="This is subtitle" /> <View android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="8dp" android:background="@color/color_divider" app:layout_constraintTop_toBottomOf="@id/imageView" /> </androidx.constraintlayout.widget.ConstraintLayout>
লক্ষ্য করুন, TextView ও ImageView তে টেক্সট ও ইমেজ দেখানোর জন্য tools attribute ব্যবহার করেছি। কারণ এটা জাস্ট একটা placeholder.
এখন আমরা এই ভিউয়ের সাথে আমাদের Kotlin কোডকে সংযুক্ত করব। আমাদের ক্লাসটির নাম দিলাম CustomView যা ConstraintLayout এর একটি subclass.
class CustomView(context: Context, @Nullable attrs: AttributeSet) : ConstraintLayout(context, attrs) { private var view : View = LayoutInflater.from(context).inflate(R.layout.custom_view, this, true) private var imageView: ImageView private var titleTextView: TextView private var subtitleTextView: TextView private var imageDrawable : Drawable? private var title: String? private var subtitle: String? init { imageView = view.imageView as ImageView titleTextView = view.titleTextView as TextView subtitleTextView = view.subtitleTextView as TextView val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.CustomView, 0, 0) try { imageDrawable = typedArray.getDrawable(R.styleable.CustomView_setImageDrawable) title = typedArray.getString(R.styleable.CustomView_setTitle) subtitle = typedArray.getString(R.styleable.CustomView_setSubTitle) imageView.setImageDrawable(imageDrawable) titleTextView.text = title subtitleTextView.text = subtitle } finally { typedArray.recycle() } /** Uncomment below line if all of your attribute fields are required. * Throw an exception if required attributes are not set. It will caused Run Time Exception. * In this sample project we assume that, no attributes are mandatory * * */ /* if (imageDrawable == null) throw RuntimeException("No image drawable provided") if (title == null) { throw RuntimeException("No title provided") } if (subtitle == null) { throw RuntimeException("No subtitle provided") }*/ } /** * Below getter-setter will work, if we need to access the attributes programmatically */ fun setImageDrawable(drawable: Drawable?) { imageView.setImageDrawable(drawable) } fun getImageDrawable() : Drawable? { return imageDrawable } fun setTitle(text: String?) { titleTextView.text = text } fun getTitle() : String? { return title } fun setSubtitle(text: String?) { subtitleTextView.text = text } fun getSubtitle() : String? { return subtitle } }
আমরা যেমন অ্যান্ড্রয়েড প্রোজেক্টে TextView ক্লাস ব্যবহার করতে পারি। CustomView ক্লাসটিকেও একই ভাবে ইউজ করতে পারব। আমরা যেমন layout ফাইলে TextView widget ইউজ করতে পারি, একই ভাবে আমাদের কাস্টম ভিউকেও ইউজ করতে পারব। xml এ যখন আমরা <CustomView> widget-টি ডিফাইন করব তখন উপরের CustomView.kt ক্লাসের constructor-টি call হবে। ভিউতে height, width সহ আর যে সকল attribute declare করব সেগুলো CustomView.kt ক্লাসের Constructor এর দ্বিতীয় আর্গুমেন্ট হিসাবে রিসিভ হবে।
ক্লাসের প্রথম লাইনেই আমাদের ডিজাইন করা custom_view.xml ভিউটি inflate করা হয়েছে। Constructor এর init{} block এর ভিতরে কাস্টম ভিউয়ের widget-গুলো (imageView, title, subtitle) initialize করা হয়েছে। এরপর xml widget থেকে কী কী attribute পাঠানো হয়েছে সেটা typedArray-তে obtain করা হচ্ছে। দেখতে পাচ্ছেন মেথডে আর্গুমেন্ট হিসাবে R.styleable.CustomView পাঠানো হচ্ছে। এটা কোথা থেকে আসল? এটা আমরা লিখে রেখেছি res/values/attrs.xml ফাইলে। নতুন প্রোজেক্ট খোলার পর res/values ডিরেক্টরিতে attr.xml পাবেন না। এটি আপনার নিজেকেই create করে নিতে হবে।
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CustomView"> <attr name="setImageDrawable" format="reference" /> <attr name="setTitle" format="string" /> <attr name="setSubTitle" format="string" /> </declare-styleable> </resources>
উপরের এই custom attribute-গুলো আমরা এখন আমাদের কাস্টম ভিউয়ের attribute হিসাবে xml ফাইলে ইউজ করতে পারব। আপনার কোনো প্রোজেক্টে যদি ৫ টা ভ্যালু সেট করার প্রয়োজন হয় সেক্ষেত্রে সেই ৫টা attribute-ই এখানে বলে দিতে হবে।
ফিরে যাই CustomView.kt ক্লাসের constructor এর init{} block এ। typedArray থেকে এরপর ভ্যালুগুলো নিয়ে সেট করা হয়েছে imageView, titleTextView ও subtitleTextView-তে। আমার এই sample project এ attribute-গুলো আমি অপশনাল রাখতে চাই। অর্থাৎ xml থেকে কোনো custom attribute না পাঠালেও যেন কোড ক্র্যাশ না করে। কিন্তু আপনি যদি আপনার কাস্টম অ্যাট্রিবিউটগুলো mandatory করে দিতে চান তাহলে constructor এর শেষের কোডগুলোর কমেন্ট উঠিয়ে দিতে পারেন।
এরপর আমরা CustomView.kt ক্লাসের শেষে আমাদের attribute-গুলোর getter -setter লিখে দিয়েছি। যেন xml এর পাশাপাশি Activity-র জাভা/কটলিন কোড থেকেও কোনো attribute programmatically সেট করা যায়।
আমাদের কাস্টম ভিউ বানানোর কাজ শেষ! এখন আমরা প্রথম ছবিতে দেখানো Activity’র UI ডিজাইন করব।
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/custom_view_label" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <com.hellohasan.androidcustomview.CustomView android:id="@+id/sunrise_custom_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" app:layout_constraintTop_toBottomOf="@+id/textView" app:setImageDrawable="@drawable/sun_rise" app:setTitle="Sun rise time - Dhaka, Bangladesh" tools:setSubTitle="5:25 AM" /> <com.hellohasan.androidcustomview.CustomView android:id="@+id/sunset_custom_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@+id/sunrise_custom_view" app:setImageDrawable="@drawable/sunset" app:setTitle="Sunset time - Dhaka, Bangladesh" tools:setSubTitle="6:12 PM" /> </androidx.constraintlayout.widget.ConstraintLayout>
দেখুন, উভয় আইটেমেই টাইটেল আর ইমেজ xml থেকে সেট করা হয়েছে। সাবটাইটেলটা tools attribute হিসাবে dummy data বসানো হয়েছে। কারণ আমরা সাবটাইটেল ফিল্ডে সূর্যোদয় ও সূর্যাস্তের সময়টা MainActivity.kt থেকে সেট করতে চাই। MainActivity.kt এর কোড হতে পারে এরকম:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // image drawable and title are already set from xml. // I want to set subtitle programmatically. you can set title and image drawable from here sunrise_custom_view.setSubtitle("5:31 AM") // sunrise time set as subtitle sunset_custom_view.setSubtitle("5:01 PM") // subset time set as subtitle } }
Conclusion
Custom view ডিজাইন করার এটাই আমার জানা মতে সবচেয়ে সহজতর উপায়। এছাড়াও canvas এ draw করে কাস্টম ভিউ বানানো যায়। আমাদের উল্লেখিত পদ্ধতিটিকে আরো উন্নত করার সুযোগ আছে। প্রয়োজনানুসারে Google করে সেটি করতে পারবেন বলেই আমার বিশ্বাস।
পুরো প্রোজেক্টটি একসাথে পাওয়া যাবে আমার গিটহাব রিপোজিটরিতে। কোথাও কোনো ভুল পরিলক্ষিত হলে বা কোনো কিছু আপডেট করার দরকার হলে কমেন্ট করতে দ্বিধা করবেন না। আপনার দুয়ায় আমাকে রাখবেন। আল্লাহ যেন আমার দুনিয়া ও আখিরাতে কল্যান দান করেন।
Awesome bro. Keep it up like always.
Thank you for your feedback
So Good to see you back with more strength. Keep it up bro.
Thank you for your feedback