ساخت کیبورد سفارشی برای اندروید با Xamarin Forms — از صفر تا صد

در برخی موارد هنگام اجرای پروژهها نیاز به کیبوردهایی داریم که بعضی امکانات خاص دارند که کیبورد معمولی اندروید فاقد آن است. در این مقاله روش ساخت کیبورد سفارشی اندروید با Xamarin Forms را توضیح خواهیم داد.
نخستین گزینهای که در این موارد به ذهن میرسد، این است که یک Entry غیر فعال با یک GestureRecognizer اضافه کنیم که یک کنترل به همراه یک انیمیشن نمایش میدهد تا ظاهر شدن کیبورد را شبیهسازی کند، اما به سرعت متوجه میشویم که این ایده قابلیت اجرای مجدد ندارد و لذا آن را کنار میگذاریم.
بهترین گزینه این است که مسیر native را پی بگیریم، اما اغلب این مسیرها در نهایت به ایجاد یک سرویس منتهی میشوند. بدیهی است که کاربران ما علاقه چندانی به این ندارند که یک کیبورد را جداگانه دانلود کنند تا صرفاً در اپلیکیشن مورد استفاده قرار دهند و باید توجه داشته باشیم که ما از Xamarin Forms استفاده میکنیم. از آنجا که میخواهیم این کیبورد سفارشی را در Xamarin Forms پیادهسازی کنیم، بهترین گزینه ممکن یک Custom Renderer (+) برای کنترل Entry است که از EditText به عنوان مبنایی برای کنترل native استفاده میکند. تلاش میکنیم تا همه دانشی که در نتیجه بررسی گزینههای مختلف به دست آمده است را در مورد این کیبورد سفارشی اندروید پیادهسازی کنیم.
کدنویسی
توجه کنید که به منظور پیادهسازی کیبورد سفارشی اندروید باید Xamarin Forms با نسخه 3.6.0.135200-pre1 یا بالاتر استفاده کنید، زیرا به متد OnFocusChangeRequest نیاز داریم که در این نسخهها موجود است. ابتدا یک کنترل سفارشی میسازیم که یک مشخصه «اتصالپذیر» bindable دارد.
در این مشخصه، خصوصیتی به نام EnterCommand و با نوع ICommand وجود دارد که برای اتصال به اکشن فشرده شدن کلید Enter استفاده میشود. اینک کنترل سفارشی ما به صورت زیر است:
namespace CustomKeyboard { public class EntryWithCustomKeyboard : Entry { public static readonly BindableProperty EnterCommandProperty = BindableProperty.Create( nameof(EnterCommand), typeof(ICommand), typeof(EntryWithCustomKeyboard), default(ICommand), BindingMode.OneWay ); public ICommand EnterCommand { get => (ICommand)GetValue(EnterCommandProperty); set => SetValue(EnterCommandProperty, value); } } }
سپس به پروژه اندروید میرویم و آنجا کار میکنیم. در ادامه دوباره به پروژه Xamarin Forms خود بازمیگردیم. پیش از ادامه باید مطمئن شویم که همه پکیجهای اندروید مورد نیاز را داریم:
سپس باید MainActivity اندروید خود را ویرایش کنیم تا از نمایش کیبورد نیتیو جلوگیری کنیم. به این منظور از خصوصیت SoftInputMode.StateAlwaysHidden استفاده میکنیم:
namespace CustomKeyboard.Droid { [Activity( Label = "CustomKeyboard", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ScreenOrientation = ScreenOrientation.Portrait, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation, WindowSoftInputMode = SoftInput.StateAlwaysHidden, LaunchMode = LaunchMode.SingleTask)] public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { TabLayoutResource = Resource.Layout.Tabbar; ToolbarResource = Resource.Layout.Toolbar; base.OnCreate(savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); LoadApplication(new App()); Window.SetSoftInputMode(SoftInput.StateAlwaysHidden); } } }
کار بعدی که انجام میدهیم آغاز تعریف کیبورد سفارشی است.
درون پوشه Resource/layout یک لیآوت اندروید به نام CustomKeyboard و از نوع InputMethodService.Keyboard ایجاد میکنیم.
<?xml version="1.0" encoding="utf-8"?> <android.inputmethodservice.Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/keyboard" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:keyPreviewLayout="@null" android:keyBackground="@drawable/keyboard_background" android:textColor="@android:color/white" android:background="@android:color/white" />
قبل از هر چیز مشخصه alignParentBottom را به صورت true تنظیم میکنیم، زیرا میخواهیم کیبورد از سمت پایین صفحه ظاهر شود. سپس مشخصه keyPreviewLayout را به صورت null تنظیم میکنیم، زیرا در این نمونه نمیخواهیم وقتی کلیدی فشرده شد، لیآوت واکنشی داشته باشد.
چنان که میبینید مشخصه keyBackground به یک drawable به نام keyboard_background اشاره میکند که وجود ندارد، بنابراین باید آن را درون پوشه Drawable به صورت یک فایل xml بسازیم. در این بخش یک سلکتور حالت برای دو حالت کلید یعنی normal (بدون فشرده شدن) و pressed (فشرده شدن) تعریف میکنیم.
<?xml version="1.0" encoding="UTF-8" ?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="false" android:state_selected="false" android:state_pressed="false" android:drawable="@drawable/normal" /> <item android:state_pressed="true" android:drawable="@drawable/pressed" /> </selector>
چنان که میبینید میخواهیم دو xml دیگر درون پوشه Drawable بسازیم که در آنها حس و ظاهر کیبورد خود را تعریف میکنیم تا با قالب اپلیکیشن هماهنگ باشد.
فایل normal.xml
<?xml version="1.0" encoding="UTF-8" ?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:left="2dp" android:right="2dp"> <shape android:shape="rectangle"> <solid android:color="#FFFFFF" /> </shape> </item> <item android:bottom="2dp"> <shape android:shape="rectangle"> <solid android:color="#FF6A00" /> </shape> </item> </layer-list>
فایل pressed.xml
<?xml version="1.0" encoding="UTF-8" ?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#FF8C00" /> </shape>
اینک درون پوشه values یک فایل xml به نام ids ایجاد میکنیم که در ادامه مورد استفاده قرار خواهد گرفت:
<?xml version="1.0" encoding="UTF-8" ?> <resources> <item name="customKeyboard" type="id" /> </resources>
سپس درون پوشه Resources یک پوشه به نام xml میسازیم. درون آن یک فایل xml ایجاد میکنیم که کلیدهای کیبورد خاص خود را در آن تعریف میکنیم.
\در این مورد کیبورد به نام special_keyboard خوانده میشود و از نوع Keyboard خواهد بود که در آن اندازه افقی و عمودی کلیدها، مشخصههای horizontalGap و verticalGap که به فاصلهبندی اشاره میکنند و همچنین نوع ابعاد %p را تعریف میکنیم که نوعی از درصد مرتبط با نمای والد است.
هر ردیف از کلیدها درون بخشهایی قرار میگیرد که با تگهای <Row></Row> از هم جدا میشوند. ردیف نخست با یک خط جداکننده اشغال شده است که محدوده کیبورد را نشان میدهد. تگ ردیف دارای ارتفاع 4dp است و بخش فوقانی کیبورد را از طریق مشخصه rowEdgeFlags نشان میدهد. سپس یک خط به صورت یک key درون تگهای Row اضافه میکنیم که عرض کامل کیبورد را اشغال میکند. جداکننده ما یک Xml دیگر است که درون پوشه Drawable ایجاد خواهیم کرد.
فایل kb_separator_line.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > <stroke android:width="1dp" /> <size android:height="2dp" /> </shape>
هر Key دو مشخصه صراحتاً ضروری به نامهای codes و keyLabel دارد. code عددی است که به سیستم عامل اعلام میکند که کلید به کدام حرف یا نماد مربوط است. کیبوردهای سفارشی مختلف از کدهای گوناگونی برای اشاره به نماد یا کلیدها استفاده میکنند؛ اما بهترین فرست کدها که اغلب نیازها را تأمین میکند Android Keycodes (+) است. میتوانید مستندات رسمی اندروید (+) و یا حتی مستندات اندروید زامارین (+) را نیز ببینید، اما هیچ کدام از آنها با دقت کار نمیکنند.
keyLabel رشتهای است که قرار است در کلید ما نمایش یابد. قرار دادن این مشخصه حتی در صورتی که قصد نمایش هیچ متنی روی کلید ندارید، حائز اهمیت بالایی است. در آن حالت میتوانید از ""=keyLabel استفاده کنید.
در مورد استایل در ابتدا و انتهای هر ردیف یک کلید با کد معادل 0 اضافه میکنیم. این کلیدها دارای مشخصه keyEdgeFlags با مقادیر left یا right مناسب هستند. کد کامل کیبورد به صورت زیر است:
فایل special_keyboard.xml
<?xml version="1.0" encoding="UTF-8" ?> <Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:keyWidth="8%p" android:keyHeight="50dp" android:horizontalGap="1%p" android:verticalGap="1%p"> <Row android:keyHeight="4dp" android:rowEdgeFlags="top" android:verticalGap="1%p"> <Key android:codes="0" android:keyWidth="100%p" android:keyIcon="@drawable/kb_separator_line" /> </Row> <Row> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" /> <Key android:codes="29" android:keyLabel="A" android:keyWidth="18%p" /> <Key android:codes="30" android:keyLabel="B" android:keyWidth="18%p" /> <Key android:codes="31" android:keyLabel="C" android:keyWidth="18%p" /> <Key android:codes="32" android:keyLabel="D" android:keyWidth="18%p" /> <Key android:codes="33" android:keyLabel="E" android:keyWidth="18%p" /> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" /> </Row> <Row> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" /> <Key android:codes="8" android:keyLabel="1" android:keyWidth="18%p" /> <Key android:codes="9" android:keyLabel="2" android:keyWidth="18%p" /> <Key android:codes="10" android:keyLabel="3" android:keyWidth="18%p" /> <Key android:codes="11" android:keyLabel="4" android:keyWidth="18%p" /> <Key android:codes="12" android:keyLabel="5" android:keyWidth="18%p" /> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" /> </Row> <Row> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" /> <Key android:codes="13" android:keyLabel="6" android:keyWidth="18%p" /> <Key android:codes="14" android:keyLabel="7" android:keyWidth="18%p" /> <Key android:codes="15" android:keyLabel="8" android:keyWidth="18%p" /> <Key android:codes="16" android:keyLabel="9" android:keyWidth="18%p" /> <Key android:codes="7" android:keyLabel="0" android:keyWidth="18%p" /> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" /> </Row> <Row> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" /> <Key android:codes="67" android:keyLabel="DELETE" android:keyWidth="37%p" /> <Key android:codes="66" android:keyLabel="ENTER" android:keyWidth="56%p" /> <Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" /> </Row> </Keyboard>
در نهایت پیش از کار روی رندر کننده پوشه دیگری درون Resources به نام anim ایجاد میکنیم که در آن یک فایل xml به نام slide_in_bottom می سازیم. انیمیشن ظاهرشدن کیبورد ما روی صفحه در این فایل قرار میگیرد:
<?xml version="1.0" encoding="UTF-8" ?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:fromYDelta="150%p" android:toYDelta="0" android:duration="200"/> <alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="200" /> </set>
اکنون یک پوشه جدید به نام Renderers روی پروژه اندروید ایجاد میکنیم و درون آن رندر کننده خود را که EntryWithCustomKeyboardRenderer نام دارد و از EntryRenderer بسط یافته و اینترفیس IOnKeyboardActionListener را پیادهسازی میکند تعریف میکنیم. همچنین درون رندر کننده سفارشی یک کلاس خصوصی به نام NullListener ایجاد میکنیم که قرار است از Java.Lang.Object بسط یابد و اینترفیس IOnKeyboardActionListener را پیادهسازی کند که در رندر کننده خود مورد استفاده قرار میدهیم تا از استثناهای null جلوگیری کنیم.
[assembly: ExportRenderer(typeof(EntryWithCustomKeyboard), typeof(EntryWithCustomKeyboardRenderer))] namespace CustomKeyboard.Droid.Renderers { public class EntryWithCustomKeyboardRenderer : EntryRenderer, IOnKeyboardActionListener { private Context context; private EntryWithCustomKeyboard entryWithCustomKeyboard; private Android.InputMethodServices.KeyboardView mKeyboardView; private Android.InputMethodServices.Keyboard mKeyboard; private InputTypes inputTypeToUse; private bool keyPressed; public EntryWithCustomKeyboardRenderer(Context context) : base(context) { this.context = context; } protected override void OnElementChanged(ElementChangedEventArgs<Entry> e) { base.OnElementChanged(e); var newCustomEntryKeyboard = e.NewElement as EntryWithCustomKeyboard; var oldCustomEntryKeyboard = e.OldElement as EntryWithCustomKeyboard; if (newCustomEntryKeyboard == null && oldCustomEntryKeyboard == null) return; if (e.NewElement != null) { this.entryWithCustomKeyboard = newCustomEntryKeyboard; this.CreateCustomKeyboard(); this.inputTypeToUse = this.entryWithCustomKeyboard.Keyboard.ToInputType() | InputTypes.TextFlagNoSuggestions; // Here we set the EditText event handlers this.EditText.FocusChange += Control_FocusChange; this.EditText.TextChanged += EditText_TextChanged; this.EditText.Click += EditText_Click; this.EditText.Touch += EditText_Touch; } // Dispose control if (e.OldElement != null) { this.EditText.FocusChange -= Control_FocusChange; this.EditText.TextChanged -= EditText_TextChanged; this.EditText.Click -= EditText_Click; this.EditText.Touch -= EditText_Touch; } } protected override void OnFocusChangeRequested(object sender, VisualElement.FocusRequestArgs e) { e.Result = true; if (e.Focus) this.Control.RequestFocus(); else this.Control.ClearFocus(); } // Event handlers private void Control_FocusChange(object sender, FocusChangeEventArgs e) { // Workaround to avoid null reference exceptions in runtime if (this.EditText.Text == null) this.EditText.Text = string.Empty; if (e.HasFocus) { this.mKeyboardView.OnKeyboardActionListener = this; if (this.Element.Keyboard == Keyboard.Text) this.CreateCustomKeyboard(); this.ShowKeyboardWithAnimation(); } else { // When the control looses focus, we set an empty listener to avoid crashes this.mKeyboardView.OnKeyboardActionListener = new NullListener(); this.HideKeyboardView(); } } private void EditText_TextChanged(object sender, Android.Text.TextChangedEventArgs e) { // Ensure no key is pressed to clear focus if (this.EditText.Text.Length != 0 && !this.keyPressed) { this.EditText.ClearFocus(); return; } } private void EditText_Click(object sender, System.EventArgs e) { ShowKeyboardWithAnimation(); } private void EditText_Touch(object sender, TouchEventArgs e) { this.EditText.InputType = InputTypes.Null; this.EditText.OnTouchEvent(e.Event); this.EditText.InputType = this.inputTypeToUse; e.Handled = true; } // Keyboard related section // Method to create our custom keyboard view private void CreateCustomKeyboard() { var activity = (Activity)this.context; var rootView = activity.Window.DecorView.FindViewById(Android.Resource.Id.Content); var activityRootView = (ViewGroup)((ViewGroup)rootView).GetChildAt(0); this.mKeyboardView = activityRootView.FindViewById<Android.InputMethodServices.KeyboardView>(Resource.Id.customKeyboard); // If the previous line fails, it means the keyboard needs to be created and added if (this.mKeyboardView == null) { this.mKeyboardView = (Android.InputMethodServices.KeyboardView)activity.LayoutInflater.Inflate(Resource.Layout.CustomKeyboard, null); this.mKeyboardView.Id = Resource.Id.customKeyboard; this.mKeyboardView.Focusable = true; this.mKeyboardView.FocusableInTouchMode = true; this.mKeyboardView.Release += (sender, e) => { }; var layoutParams = new Android.Widget.RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.WrapContent); layoutParams.AddRule(LayoutRules.AlignParentBottom); activityRootView.AddView(this.mKeyboardView, layoutParams); } this.HideKeyboardView(); this.mKeyboard = new Android.InputMethodServices.Keyboard(this.context, Resource.Xml.special_keyboard); this.SetCurrentKeyboard(); } private void SetCurrentKeyboard() { this.mKeyboardView.Keyboard = this.mKeyboard; } // Method to show our custom keyboard private void ShowKeyboardWithAnimation() { // First we must ensure that keyboard is hidden to // prevent showing it multiple times if (this.mKeyboardView.Visibility == ViewStates.Gone) { // Ensure native keyboard is hidden var imm = (InputMethodManager)this.context.GetSystemService(Context.InputMethodService); imm.HideSoftInputFromWindow(this.EditText.WindowToken, 0); this.EditText.InputType = InputTypes.Null; var animation = AnimationUtils.LoadAnimation(this.context, Resource.Animation.slide_in_bottom); this.mKeyboardView.Animation = animation; this.mKeyboardView.Enabled = true; // Show custom keyboard with animation this.mKeyboardView.Visibility = ViewStates.Visible; } } // Method to hide our custom keyboard private void HideKeyboardView() { this.mKeyboardView.Visibility = ViewStates.Gone; this.mKeyboardView.Enabled = false; this.EditText.InputType = InputTypes.Null; } // Implementing IOnKeyboardActionListener interface public void OnKey([GeneratedEnum] Keycode primaryCode, [GeneratedEnum] Keycode[] keyCodes) { if (!this.EditText.IsFocused) return; // Ensure key is pressed to avoid removing focus this.keyPressed = true; // Create event for key press long eventTime = JavaSystem.CurrentTimeMillis(); var ev = new KeyEvent(eventTime, eventTime, KeyEventActions.Down, primaryCode, 0, 0, 0, 0, KeyEventFlags.SoftKeyboard | KeyEventFlags.KeepTouchMode); // Ensure native keyboard is hidden var imm = (InputMethodManager)this.context.GetSystemService(Context.InputMethodService); imm.HideSoftInputFromWindow(this.EditText.WindowToken, HideSoftInputFlags.None); this.EditText.InputType = this.inputTypeToUse; switch(ev.KeyCode) { case Keycode.Enter: // Sometimes EditText takes long to update the HasFocus status if (this.EditText.HasFocus) { // Close the keyboard, remove focus and launch command asociated action this.HideKeyboardView(); this.ClearFocus(); this.entryWithCustomKeyboard.EnterCommand?.Execute(null); } break; } // Set the cursor at the end of the text this.EditText.SetSelection(this.EditText.Text.Length); if (this.EditText.HasFocus) { this.DispatchKeyEvent(ev); this.keyPressed = false; } } public void OnPress([GeneratedEnum] Keycode primaryCode) { } public void OnRelease([GeneratedEnum] Keycode primaryCode) { } public void OnText(ICharSequence text) { } public void SwipeDown() { } public void SwipeLeft() { } public void SwipeRight() { } public void SwipeUp() { } private class NullListener : Java.Lang.Object, IOnKeyboardActionListener { public void OnKey([GeneratedEnum] Keycode primaryCode, [GeneratedEnum] Keycode[] keyCodes) { } public void OnPress([GeneratedEnum] Keycode primaryCode) { } public void OnRelease([GeneratedEnum] Keycode primaryCode) { } public void OnText(ICharSequence text) { } public void SwipeDown() { } public void SwipeLeft() { } public void SwipeRight() { } public void SwipeUp() { } } } }
در نهایت به پروژه Xamarin Forms باز میگردیم و کیبورد خاص خود را پیادهسازی میکنیم:
فایل MainPage.xaml
<?xml version="1.0" encoding="utf-8"?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:CustomKeyboard" x:Class="CustomKeyboard.MainPage"> <ScrollView> <StackLayout Orientation="Vertical"> <local:EntryWithCustomKeyboard x:Name="entry1" HorizontalOptions="FillAndExpand" Margin="0, 0, 0, 20" Keyboard="Text" TextColor="Black" Placeholder="Custom Keyboard entry..." /> <local:EntryWithCustomKeyboard x:Name="entry2" HorizontalOptions="FillAndExpand" Keyboard="Text" TextColor="Black" Placeholder="Custom Keyboard entry 2..." /> </StackLayout> </ScrollView> </ContentPage>
همچنین در کد قبلی خود EnterCommand را که پیشتر ساختیم پیادهسازی میکنیم. بدین ترتیب اکشنی که میخواهیم هنگام فشردن کلید اینتر کیبورد سفارشی انجام یابد را تعریف میکنیم:
فایل MainPage.cs
namespace CustomKeyboard { public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); // Here we implement the action of the Enter button on our custom keyboard this.entry1.EnterCommand = new Command(() => this.entry2.Focus()); this.entry2.EnterCommand = new Command(() => this.entry1.Focus()); } } }
نتیجه نهایی به صورت زیر است:
بدین ترتیب به پایان راهنما میرسیم. برای مشاهده کد کامل این پروژه به این ریپوی گیتهاب (+) مراجعه کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای پروژهمحور برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامهنویسی اندروید (Android) – پیشرفته
- روشهای اجرای اپلیکیشنهای اندروید در ویندوز — راهنمای کاربردی
- نکات کلیدی اندروید ۱۰ برای توسعهدهندگان — راهنمای کاربردی
==
درود استاد
بنده یکم گیج شدم اگه میشه لطف کنید فیلم همین کارو بذارین که راحت تر بتونم بفهممش…
مورد ضروری دمتون گرم لطفاً کمک کنید🙏🏻
سلام
آموزش ساخت کیبورد واقعا خیلی خوب بود ممنون
خیلی زیبا آموزش دادین ، خیلی ممنون برای این آموزشتون
دمتون گرم عالی
سلام لطفا اگر کسی برنانکه نویسی کیبورد سفارشی بلده بسازه به ایمیل من پیام بده بهم یاد بده خیلی ازش ممنون میشم