پروژه‌های کاربردی در اندروید: ۳- برنامه محاسبه دنگ

گروهی دوستانه برای صرف شام به رستوران می­‌روند. برای محاسبه هزینه­‌ها و مشخص کردن دنگ هر فرد در این گروه، نیاز به محاسبات دستی برای درصد انعام و تقسیم مبلغ نهایی به تعداد افراد است. برنامه Tipster برای راحتی چنین محاسباتی پیاده‌سازی شده است تا در این برنامه با کدنویسی اندروید، عناصر گرافیکی رابط کاربری و انجام محاسبات ساده آشنایی بیش­تری کسب گردد. نمایی از برنامه Tipsterتوضیحاتدر این قسمت برنامه Tipster با جزئیات کامل بررسی خواهد شد.ساخت فایل Layout با استفاده از ویجت‌­هاهدف نهایی ساخت Layoutای مانند شکل قبل است. برای رسیدن به چنین طرحی، نیاز است که از چندین ویجت­ رابط کاربری، که در ادامه لیست شده­، استفاده کنید.TableLayout: این ویجت، عناصر نما را در یک چینش جدولی قرار می­‌دهد.TableRow: یک سطر در TableLayout تعریف می‌کند.TextView:یک برچسب متنی برای نمایش متنی در صفحه‌­نمایش است.EditText:یک فیلد متنی برای دریافت مقداری از ورودی می­‌باشد.RadioGroup:تعدادی دکمه رادیویی در قالب یک گروه ایجاد می­‌کند که در هر لحظه فقط یکی از آن­‌ها می­‌تواند انتخاب شود.RadioButton:یک دکمه رادیویی برای استفاده در RadioGroup ایجاد می‌­کند.Button:یک دکمه معمولی می‌­سازد.View:یک جداکننده با ارتفاع و رنگ مشخص برای استفاده در بین ویجت‌­های دیگر ایجاد می‌­کند.فایل Layout اصلی برنامه به نام activity_main.xml، در زمان ساخت پروژه، به صورت پیش‌­فرض ساخته شده است. کدهای زیر را به این فایل اضافه کنید. ویجت TableRow یک سطر درون TableLayout ایجاد می‌کند و بنا به نیاز برنامه هر تعدادی از این ویجت را می­‌توانید به برنامه اضافه کنید. پنج سطر برای ورودی‌­های برنامه و دکمه‌­ها ایجاد و بعد از خط جداکننده، ۳ سطر دیگر را نیز برای نمایش نتایج، به کدهای XML اضافه کنید.<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/TableLayout01" android:layout_width="fill_parent" android:layout_height="fill_parent" android:stretchColumns="1"> <TableRow> <TextView android:id="@+id/txtLbl1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="0" android:text="@string/total_amount" /> <EditText android:id="@+id/txtAmount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="1" android:layout_span="3" android:inputType="number" /> </TableRow> <TableRow> <TextView android:id="@+id/txtLbl2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="0" android:text="@string/no_of_people" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:="decrement" android:text="@string/minus" /> <EditText android:id="@+id/txtPeople" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="number" android:text="@string/initial_value" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:="increment" android:text="@string/plus" /> </TableRow> <TableRow> <TextView android:id="@+id/txtLbl3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/tip_percentage" /> </TableRow> <TableRow> <RadioGroup android:id="@+id/RadioGroupTips" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="0" android:layout_span="3" android:checkedButton="@+id/radioFifteen" android:orientation="horizontal"> <RadioButton android:id="@+id/radioFifteen" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/_15prc" android:textSize="15sp" /> <RadioButton android:id="@+id/radioTwenty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/_20prc" android:textSize="15sp" /> <RadioButton android:id="@+id/radioOther" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/other" android:textSize="15sp" /> </RadioGroup> <EditText android:id="@+id/txtTipOther" android:layout_width="fill_parent" android:layout_height="wrap_content" android:inputType="number" /> </TableRow> <TableRow> <Button android:id="@+id/btnReset" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="2" android:text="@string/btn_reset" /> <Button android:id="@+id/btnCalculate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="3" android:text="@string/btn_calculate" /> </TableRow> <View android:layout_height="2dp" android:layout_marginBottom="5dip" android:layout_marginTop="5dip" android:background="#DDFFDD" /> <TableRow android:paddingBottom="10dip" android:paddingTop="5dip"> <TextView android:id="@+id/txtLbl4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="0" android:text="@string/tip_amount" /> <TextView android:id="@+id/txtTipAmount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="2" android:layout_span="2" /> </TableRow> <TableRow android:paddingBottom="10dip" android:paddingTop="5dip"> <TextView android:id="@+id/txtLbl5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="0" android:text="@string/total_to_pay" /> <TextView android:id="@+id/txtTotalToPay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="2" android:layout_span="2" /> </TableRow> <TableRow android:paddingBottom="10dip" android:paddingTop="5dip"> <TextView android:id="@+id/txtLbl6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="0" android:text="@string/total_per_person" /> <TextView android:id="@+id/txtTipPerPerson" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="2" android:layout_span="2" /> </TableRow> </TableLayout>فایل Layout برنامه Tisterجدول و سطرهاتا این مرحله به فایل activity_main.xml یک TableLayout اضافه شده است. در این جدول هر تعداد دلخواهی سطر می­‌تواند قرار گیرد و سپس ویجتٰ­های دیگری مانند دکمه‌­ها، برچسب‌­ها و فیلدهای متنی در سطرها قرار خواهند گرفت. به ویژگی­‌های android:stretchColumns، android:layout_column و android:layout_span دقت کنید.کنترل ورودی­‌های برنامهاولین ویجت EditText برنامه (با شناسه txtAmount) برای دریافت مبلغ کل از کاربر استفاده می­‌شود. ورودی این ویجت برای دریافت مبلغ باید از جنس عدد باشد، بنابراین ویژگی android:numeric را با number مقداردهی کنید. با تنظیم این ویژگی کاربر مجاز است که فقط مقادیر عددی را وارد کند. این ویژگی را برای ویجت EditText دوم نیز که مربوط به تعداد افراد است، تنظیم کنید. با استفاده از این ویژگی­‌ها کنترل‌­های لازم بر روی ورودی، به سادگی در همان کدهای XML صورت گیرد.کدهای جاوافایل TipsterActivity.java کلاس اصلی برنامه است و نمایش Layout و مدیریت برنامه را برعهده دارد. کدهای کلاس TipsterActivity به چند بخش دسته­‌بندی می­‌شوند. در بخش ۱ و ۲، ابتدا ویجت­‌های موردنیاز را به عنوان ویژگی از کلاس تعریف کنید. سپس با کمک تابع findViewById(int id) ویجت موردنظر را دریافت کرده و در متغیر مربوط به خود ذخیره نمائید (بخش ۳ و ۶). شناسه هر ویجت در فایل Layout مشخص شده است.public class TipsterActivity extends Activity { final static int DEFAULT_NUM_PEOPLE = 3; final static NumberFormat formatter = NumberFormat.getCurrencyInstance(); // Section 1: Widgets in the application private EditText txtAmount; private EditText txtPeople; private EditText txtTipOther; private RadioGroup rdoGroupTips; private Button btnCalculate; private Button btnReset; // Section 2 private TextView txtTipAmount; private TextView txtTotalToPay; private TextView txtTipPerPerson; private int radioCheckedId = -1; private NumberPickerLogic mLogic; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Section 3 txtAmount = (EditText) findViewById(R.id.txtAmount); // Section 4 txtAmount.requestFocus(); txtPeople = (EditText) findViewById(R.id.txtPeople); txtPeople.setText(String.valueOf(DEFAULT_NUM_PEOPLE)); txtTipOther = (EditText) findViewById(R.id.txtTipOther); rdoGroupTips = (RadioGroup) findViewById(R.id.RadioGroupTips); btnCalculate = (Button) findViewById(R.id.btnCalculate); // Section 5 btnCalculate.setEnabled(false); btnReset = (Button) findViewById(R.id.btnReset); // Section 6 txtTipAmount = (TextView) findViewById(R.id.txtTipAmount); txtTotalToPay = (TextView) findViewById(R.id.txtTotalToPay); txtTipPerPerson = (TextView) findViewById(R.id.txtTipPerPerson); // Section 7 txtTipOther.setEnabled(false);…}}بخش اول کدهای کلاس اصلی TipsterActivityدر بخش ۴ تابع requestFocus() مشاهده می­‌شود. این تابع مربوط به کلاس View است و از آن­‌جا که ویجت EditText نیز از کلاس View مشتق شده است، برای آن نیز مناسب می­‌باشد. با استفاده از این تابع، بعد از اجرای برنامه تمرکز رابط کاربری بر روی ویجت مبلغ کل است و نشانه‌گر در این قسمت آماده دریافت داده از کاربر می­‌باشد.دکمه محاسبه از ابتدای اجرای برنامه غیرفعال است (بخش ۵ از کد، با استفاده از تابع setEnabled(Boolean enabled)). قبل از کلیک بر روی این دکمه نیاز است که اطلاعات دیگری از رابط کاربری (مانند مبلغ کل و تعداد افراد) کامل شوند، به همین دلیل این دکمه در ابتدای اجرای برنامه، غیر فعال خواهد بود.به بخش ۷ توجه کنید. فیلد متنی مربوط به وارد کردن درصد غیرفعال شده است. با اجرای برنامه به صورت پیش­‌فرض دکمه ۱۵% از گروه دکمه‌­های رادیویی فعال است و این مورد در کدهای XML با ویژگی زیر تعیین شده است.android:checkedButton="@+id/radioFifteen"در صورتی که کاربر از این گروه، گزینه Other را انتخاب کند، فیلد متنی مربوط به آن نیز فعال خواهد شد.رخدادهای رابط کاربریابتدا گروه دکمه‌­های رادیویی را بررسی کنید. برنامه باید به رخدادهایی که در هر کدام از اجزای این گروه صورت می‌­گیرد، گوش دهد و طبق این رخدادها تصمیم­‌گیری نماید. گوش دادن به رخدادها با استفاده از تابع OnCheckedChangeListener() امکان­‌پذیر است.در این تابع چندین مورد باید مدیریت شود. یکی این­‌که به محض انتخاب گزینه Other توسط کاربر فیلد متنی مربوط به درصد نیز فعال شود و اگر کاربر دوباره به دو گزینه دیگر برگردد، فیلد متنی غیرفعال گردد. از طرف دیگر دکمه Calculate، فقط در صورتی که مقادیر وارد شده Total Amount و No. of People صحیح باشند و یکی از دو حالت زیر اتفاق افتد، باید فعال و در غیر این­‌صورت غیرفعال گردد:گزینه Other از گروه دکمه‌­های رادیویی انتخاب شده باشد و در فیلد متنیِ درصد نیز مقدار صحیحی وارد گردد.یکی از گزینه‌­های ۱۵% و یا ۲۰% انتخاب شده باشد.rdoGroupTips.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { if (checkedId == R.id.radioFifteen || checkedId == R.id.radioTwenty) { txtTipOther.setEnabled(false); btnCalculate.setEnabled(txtAmount.getText().length() > 0 && txtPeople.getText().length() > 0); } if (checkedId == R.id.radioOther) { txtTipOther.setEnabled(true); txtTipOther.requestFocus(); btnCalculate.setEnabled(txtAmount.getText().length() > 0 && txtPeople.getText().length() > 0 && txtTipOther.getText().length() > 0); } radioCheckedId = checkedId; } });بخش دوم کدهای کلاس اصلی TipsterActivityبررسی فعالیت کلیدها در فیلدهای متنیهمان‌طور که قبلا نیز گفته شد، دکمه Calculate تا زمانی که مقدار فیلدهای متنی صحیح نباشد، نباید فعال گردد. از آن­‌جا که ویژگی android:numeric برای ویجت‌­ها تنظیم شده است، بنابراین نگرانی در مورد نوع ورودی‌­های غیرعددی ندارید. پس فقط کافی­ست که از خالی نبودن فیلدها مطمئن شوید.برای آگاهی از رخداد فشرده شدن یک کلید از رابط ایستای OnKeyListener() استفاده کنید. با استفاده از این رابط می­‌توان از رخداد قبل از این­که برای ویجت فیلد متنی ارسال شود، آگاه شد. تابع setOnKeyListener(…) را برای ویجت‌­های موردنظر فراخوانی کنید.txtAmount.setOnKeyListener(mKeyListener);txtPeople.setOnKeyListener(mKeyListener);txtTipOther.setOnKeyListener(mKeyListener);بخش سوم کدهای کلاس اصلی TipsterActivityو سپس پیاده‌­سازی تابع mKeyListener رانجام دهید.private OnKeyListener mKeyListener = new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { switch (v.getId()) { case R.id.txtAmount: case R.id.txtPeople: btnCalculate.setEnabled(txtAmount.getText().length() > 0 && txtPeople.getText().length() > 0); break; case R.id.txtTipOther: btnCalculate.setEnabled(txtAmount.getText().length() > 0 && txtPeople.getText().length() > 0 && txtTipOther.getText().length() > 0); break; } return false; } };بخش سوم کدهای کلاس اصلی TipsterActivityدر این کد ابتدا شناسه نما بررسی می‌شود. به یاد آورید که هر ویجت یک شناسه منحصر به فرد دارد که در فایل Layout مربوط به آن (activity_mani.xml) تنظیم شده است. در گام بعدی بررسی شده است که اگر رخداد مربوط به ویجت مبلغ کل و یا تعداد افراد است، در صورت مقدار داشتن هر دو فیلد، کلید Calculate فعال گردد. سپس به سراغ گزینه Other در دکمه‌­های رادیویی رفته است. در این حالت علاوه بر شرایط قبلی، متن مربوط به درصد نیز باید مقدار داشته باشد.گوش کردن به کلیک­‌های دکمهمراحل ذکر شده در بخش قبلی را در مورد دکمه­‌های Calculate و Reset نیز پیاده­‌سازی کنید. در این بخش از رابط ایستای Listener() استفاده کنید و درون این تابع براساس شناسه دکمه، تابع calculate() و یا reset() را فراخوانی نمائید. در ادامه ابتدا تابع setListener(…) را برای دکمه­‌ها تنظیم کنید.btnCalculate.setListener(mClickListener);btnReset.setListener(mClickListener);بخش چهارم کدهای کلاس اصلی TipsterActivityسپس پیاده‌­سازی این تابع را انجام دهید.private Listener mClickListener = new Listener() { @Override public void (View v) { if (v.getId() == R.id.btnCalculate) { calculate(); } else { reset(); } } };بخش پنجم کدهای کلاس اصلی TipsterActivityتنظیم دوباره فیلدهازمانی که کاربر بر روی دکمه Reset کلیک می­‌کند، تمامی مقادیر فیلدهای متنی باید حذف گردد، دکمه‌­های رادیویی به مقدار پیش­‌فرض خود یعنی ۱۵% برگردد و نتیجه محاسبات نیز از رابط کاربری پاک شود.private void reset() { txtTipAmount.setText(""); txtTotalToPay.setText(""); txtTipPerPerson.setText(""); txtAmount.setText(""); txtPeople.setText(String.valueOf(DEFAULT_NUM_PEOPLE)); txtTipOther.setText(""); rdoGroupTips.clearCheck(); rdoGroupTips.check(R.id.radioFifteen); // set focus on the first field txtAmount.requestFocus(); }بخش ششم کدهای کلاس اصلی TipsterActivityبررسی صحت ورودی برای محاسباتعلاوه بر محدودیت­‌هایی که در مورد مقادیر ورودی‌­های متنی اعمال کردید، بررسی دیگری نیز برای محاسبات لازم است. مقدار فیلدهای مبلغ کل، تعداد افراد و مقدار درصد انعام (در صورت فعال بودن گزینه سوم دکمه‌های رادیویی) نباید صفر باشد، زیرا در محاسبات خطای تقسیم به صفر رخ خواهد داد.بنابراین اگر کاربر برای فیلدهای موردنظر مقدار صفر را وارد کند، با استفاده از تابع showErrorAlert(String errorMessage, final int fieldId) یک پنجره Pop-up در مورد خطای صورت گرفته، به او نمایش دهید.private void calculate() { Double billAmount = Double.parseDouble(txtAmount.getText().toString()); Double totalPeople = Double.parseDouble(txtPeople.getText().toString()); Double percentage = null; boolean isError = false; if (billAmount < 1.0) { showErrorAlert("Enter a valid Total Amount.", txtAmount.getId()); isError = true; } if (totalPeople < 1.0) { showErrorAlert("Enter a valid number of people.", txtPeople.getId()); isError = true; } if (radioCheckedId == -1) { radioCheckedId = rdoGroupTips.getCheckedRadioButtonId(); } if (radioCheckedId == R.id.radioFifteen) { percentage = 15.00; } else if (radioCheckedId == R.id.radioTwenty) { percentage = 20.00; } else if (radioCheckedId == R.id.radioOther) { percentage = Double.parseDouble( txtTipOther.getText().toString()); if (percentage < 1.0) { showErrorAlert("Enter a valid Tip percentage", txtTipOther.getId()); isError = true; } } if (!isError) { double tipAmount = ((billAmount * percentage) / 100); double totalToPay = billAmount + tipAmount; double perPersonPays = totalToPay / totalPeople; txtTipAmount.setText(formatter.format(tipAmount)); txtTotalToPay.setText(formatter.format(totalToPay)); txtTipPerPerson.setText(formatter.format(perPersonPays)); } }بخش هفتم کدهای کلاس اصلی TipsterActivityمقدار پیش­‌فرض دکمه‌­های رادیویی ۱۵% است و در صورتی که کاربر این مقدار را تغییر دهد، شناسه دکمه مربوطه در متغیر radioCheckedId ذخیره خواهد شد. اما اگر کاربر مقدار پیش‌­فرض را تغییری ندهد، متغیر radioCheckedId دارای ارزش 1- است (مقدار اولیه متغیر). بنابراین می‌­توان با منفی بود این متغیر پی برد که دکمه رادیویی فعال همان پیش‌­فرض (۱۵%) است و یا این‌که با فراخوانی تابع getCheckedRadioButtonId()، شناسه مربوط به آن را استخراج نمود.نمایش نتایج و هشدارهامحاسبات دنگ بسیار ساده است. اگر خطایی رخ ندهد (isError = false)، محاسبات انجام شده و در ویجت‌­های فیلد متنی نمایش داده می­‌شود.در اندروید کلاسی به نام AlertDialog وجود دارد که با استفاده از آن می­‌توان یک پنجره Pop-up را برای هشدار نمایش داد. این پنجره دارای سه دکمه و یک متن است. تابع showErrorAlert() پیاده‌­سازی شده است. این تابع یک رشته به عنوان متن خطا برای نمایش به کاربر و یک شناسه مربوط به فیلدی که خطا در آن رخ داده است را به عنوان ورودی دریافت می‌­کند تا بعد از پایان نمایش پنجره خطا، نشانه‌­گر بر روی این فیلد نمایش داده شود.private void showErrorAlert(String errorMessage, final int fieldId) { new AlertDialog.Builder(this) .setTitle("Error") .setMessage(errorMessage) .setNeutralButton("Close", new DialogInterface.Listener() { @Override public void (DialogInterface dialog, int which) { findViewById(fieldId).requestFocus(); } }).show(); }بخش هفتم کدهای کلاس اصلی TipsterActivityمی‌توانید پروژه Tipsterرا از این‌جا دانلود کنید.برای کسب اطلاعات بیش‌تر در زمینه برنامه‌نویسی اندروید می‌توانید به منابع زیر مراجعه کنید:آموزش جامع برنامه نویسی اندروید (مقدماتی)آموزش جامع برنامه نویسی اندروید (پیشرفته)پروژه های کاربردی در اندرویدمنابع الکترونیکی:آموزش جامع برنامه نویسی اندروید (مقدماتی)آموزش جامع برنامه نویسی اندروید (پیشرفته)پروژه های کاربردی در اندروید