Programming     Travel Logs     Life Is Good     Surfing Online     About Me
Fortunes require leverage. Business leverage comes from capital, people, and products with no marginal cost of replication (code and media).
-Naval Ravikant
2018-07-17 17:55:09

Copy this link when reproducing:
http://www.casperlee.com/en/y/blog/201

The system already allows the user to initialize all the categories when he or she runs the application for the first time. But this is not enough. I need to implement a module which allows a user to add or edit a category at any time. It is very straightforward, so there is no extra explanations needed, I'll just list all the changes of the code in this article, in stead of explaining too much. Just like before, let's take it easy and enjoy a few beautiful photos first.

/Images/20171004/01.jpg

/Images/20171004/02.jpg

/Images/20171004/03.jpg

/Images/20171004/04.jpg

/Images/20171004/05.jpg

/Images/20171004/06.jpg

1. Open the file "app -> res -> values -> strings.xml", add the following strings into it:

<string name="mi_categories_title">Categories</string>
    <string name="mi_modify_title">Edit</string>
    <string name="mi_delete_title">Delete</string>
    <string name="btn_add_caption2">Add</string>
    <string name="caption_limit">Limit</string>
    <string name="caption_name">Name</string>
    <string name="category_input_hint">Format: [Name] [Limit]</string>
    <string name="category_input_invalid">Your input is invalid!</string>
    <string name="category_duplicate">Duplicated!</string>
    <string name="category_already_used">The category is being used!</string>
    <string name="delete_category_hint">Are you sure you want to delete this category?</string>
    <string name="dialog_confirm_title">Confirm</string>

2. Open the file "app -> res -> values -> string.xml (zh)", add the following strings into it:

<string name="mi_categories_title">分类维护</string>
    <string name="mi_modify_title">修改</string>
    <string name="mi_delete_title">删除</string>
    <string name="btn_add_caption2">添加</string>
    <string name="caption_limit">限额:</string>
    <string name="caption_name">名称:</string>
    <string name="category_input_hint">请输入名称和限额,中间用空格分开,如:彩票  50</string>
    <string name="category_input_invalid">输入的信息不合法!</string>
    <string name="category_duplicate">类别名称重复!</string>
    <string name="category_already_used">该类别已经在使用了!</string>
    <string name="delete_category_hint">你真的要删除此项么?</string>
    <string name="dialog_confirm_title">确认</string>

3. Open the file "app -> res -> values -> styles.xml", add the following styles into it:

<style name="LargeWhiteText" parent="android:Widget.TextView">
        <item name="android:textAppearance">?android:attr/textAppearanceLarge</item>
        <item name="android:textColor">#FFFFFFFF</item>
    </style>
    <style name="MediumWhiteText" parent="android:Widget.TextView">
        <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
        <item name="android:textColor">#FFFFFFFF</item>
    </style>
    <style name="SmallWhiteText" parent="android:Widget.TextView">
        <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
        <item name="android:textColor">#FFFFFFFF</item>
    </style>

4. Open the file "app -> res -> menu-> main.xml", add the following menu item into it:

<item android:id="@+id/miCategories" android:title="@string/mi_categories_title"/>

5. Add a new menu resource file named "activity_modify_category_listview_menu.xml" in the folder "app -> res -> menu", and put the following text in:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_modify"
        android:title="@string/mi_modify_title" />
    <item
        android:id="@+id/menu_delete"
        android:title="@string/mi_delete_title" />
</menu>

6. Add a new class named "GlobalFuncs" in the folder "app -> java -> com.casperlee.personalexpense -> global", and put the following code in:

package com.casperlee.personalexpense.global;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.util.DisplayMetrics;
import android.view.View;

import com.casperlee.personalexpense.R;

public class GlobalFuncs {

    public static void ShowConfirm(Context context, String message,
                                   OnClickListener positiveOnClick, OnClickListener negativeOnClick) {

        new AlertDialog.Builder(context)
                .setIcon(R.mipmap.ic_launcher)
                .setTitle(R.string.dialog_confirm_title).setMessage(message)
                .setPositiveButton(R.string.btn_ok_caption, positiveOnClick)
                .setNegativeButton(R.string.btn_cancel_caption, negativeOnClick)
                .create().show();
    }
}

7. Add a new Activity class named "ModifyCategoryActivity" in the folder "app -> java -> com.casperlee.personalexpense", and put the following code in:

package com.casperlee.personalexpense;

import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Layout;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.casperlee.personalexpense.bll.CategoryBll;
import com.casperlee.personalexpense.dal.CategoryDal;
import com.casperlee.personalexpense.entities.CategoryEntity;
import com.casperlee.personalexpense.global.GlobalFuncs;
import com.casperlee.personalexpense.global.GlobalVars;

public class ModifyCategoryActivity extends AppCompatActivity {

    // Constants
    public static final String KEY_INVOKE_TYPE = "InvokeType";
    public static final int INVOKE_TYPE_ADD = 0;
    public static final int INVOKE_TYPE_MANAGE = 1;

    // Fields
    private Layout ui;
    private CategoryEntity[] categories = new CategoryEntity[0];
    private CategoryEntity currentSelectedCategory;
    private int invokeType = 0;

    // Entrances
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_modify_category);

        this.readExtras();
        this.ui = new Layout();
        this.registerForContextMenu(ui.CategorieslistView);
        this.setListeners();
    }

    @Override
    protected void onResume() {
        if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }
        super.onResume();

        this.loadCategories();
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {

        switch (v.getId()) {

            case R.id.CategorieslistView:

                MenuInflater inflater = getMenuInflater();
                inflater.inflate(R.menu.activity_modify_category_listview_menu, menu);
                break;
        }

        super.onCreateContextMenu(menu, v, menuInfo);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {

        AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
        ModifyCategoryActivity.this.currentSelectedCategory = (CategoryEntity)ModifyCategoryActivity.this.ui.CategorieslistView.getItemAtPosition(
                menuInfo.position);

        switch (item.getItemId()) {

            case R.id.menu_delete:
                GlobalFuncs.ShowConfirm(this, getString(R.string.delete_category_hint), new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                        if (CategoryDal.getInstance().categoryAlreadyUsed(ModifyCategoryActivity.this.currentSelectedCategory)) {

                            Toast.makeText(ModifyCategoryActivity.this, R.string.category_already_used, Toast.LENGTH_LONG).show();
                            return;
                        }

                        CategoryDal.getInstance().del(ModifyCategoryActivity.this.currentSelectedCategory);
                        ModifyCategoryActivity.this.loadCategories();
                        GlobalVars.Categories = CategoryBll.getInstance().GetCategories(true);
                    }

                }, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }

                });
                break;

            case R.id.menu_modify:

                Intent intent = new Intent();
                intent.setClass(ModifyCategoryActivity.this, ModifySingleCategoryActivity.class);
                intent.putExtra(ModifySingleCategoryActivity.KEY_CATEGORY_ID, ModifyCategoryActivity.this.currentSelectedCategory.getId());
                intent.putExtra(ModifySingleCategoryActivity.KEY_CATEGORY_NAME, ModifyCategoryActivity.this.currentSelectedCategory.getName());
                intent.putExtra(ModifySingleCategoryActivity.KEY_CATEGORY_LIMIT, ModifyCategoryActivity.this.currentSelectedCategory.getLimit());
                ModifyCategoryActivity.this.startActivity(intent);
                break;
        }

        return super.onContextItemSelected(item);
    }

    // Private functions
    private void readExtras() {

        this.invokeType = this.getIntent().getIntExtra(KEY_INVOKE_TYPE, INVOKE_TYPE_MANAGE);
    }
    private void setListeners() {

        this.setViewClickListeners();
        this.setItemClickListeners();
        this.setItemLongClickListeners();
    }
    private void loadCategories() {

        this.categories = CategoryBll.getInstance().GetCategories(false);
        CategoryAdapter adapter = new CategoryAdapter();
        adapter.setData(this.categories);
        this.ui.CategorieslistView.setAdapter(adapter);
        adapter.notifyDataSetChanged();
    }
    private void setViewClickListeners() {

        OnMyViewClickListener l = new OnMyViewClickListener();
        this.ui.OperateButton.setOnClickListener(l);
    }
    private void setItemClickListeners() {

        OnMyItemClickListener l = new OnMyItemClickListener();
        this.ui.CategorieslistView.setOnItemClickListener(l);
    }
    private void setItemLongClickListeners() {

        OnMyItemLongClickListener l = new OnMyItemLongClickListener();
        this.ui.CategorieslistView.setOnItemLongClickListener(l);
    }
    private boolean readNewCategory(CategoryEntity category) {

        String categoryString = this.ui.CategoryEditText.getText().toString().trim();
        if (categoryString.compareTo("") == 0) {

            return false;
        }

        String[] strs = categoryString.split(" ");
        if (strs.length > 0) {

            category.setName(strs[0].trim());
            category.setLimit(0);
            if (strs.length > 1) {

                try {

                    double d = Double.parseDouble(strs[1]);
                    category.setLimit(d);

                } catch (NumberFormatException ex) {

                    return false;
                }
            }
        }

        return true;
    }
    private boolean duplicateCategory(CategoryEntity c) {

        for (int i = 0; i < this.ui.CategorieslistView.getCount(); i++) {

            if (((CategoryEntity)this.ui.CategorieslistView.getItemAtPosition(i)).getName().trim().compareTo(c.getName().trim()) == 0) {

                return true;
            }
        }

        return false;
    }

    // Event handlers
    private void PerformOperateButtonClick() {

        CategoryEntity category = new CategoryEntity();
        if (!this.readNewCategory(category)) {

            Toast.makeText(this, R.string.category_input_invalid, Toast.LENGTH_LONG).show();
            return;
        }

        if (this.duplicateCategory(category)) {

            Toast.makeText(this, R.string.category_duplicate, Toast.LENGTH_LONG).show();
            return;
        }

        CategoryDal.getInstance().add(category);
        GlobalVars.Categories = CategoryBll.getInstance().GetCategories(true);
        if (this.invokeType == INVOKE_TYPE_ADD) {

            AddExpenseActivity.getInstance().setNeedShowLastCategory(true);
            this.finish();

        } else {

            this.loadCategories();
            this.ui.CategoryEditText.setText(null);
        }
    }

    // Classes
    private class Layout {

        public EditText CategoryEditText = null;
        public Button OperateButton = null;
        public ListView CategorieslistView = null;

        public Layout() {

            this.initialize();
        }

        private void initialize() {

            this.CategoryEditText = (EditText)ModifyCategoryActivity.this.findViewById(R.id.CategoryEditText);
            this.OperateButton = (Button)ModifyCategoryActivity.this.findViewById(R.id.OperateButton);
            this.CategorieslistView = (ListView)ModifyCategoryActivity.this.findViewById(R.id.CategorieslistView);
        }
    }
    private class CategoryAdapter extends BaseAdapter {

        private CategoryEntity[] entities;
        private LayoutInflater inflater;
        private int selectedIndex = -1;

        public CategoryAdapter() {

            this.inflater = getLayoutInflater();
            this.entities = new CategoryEntity[0];
        }

        @Override
        public int getCount() {

            return this.entities.length;
        }

        @Override
        public Object getItem(int position) {

            if (position < 0 || position > this.entities.length - 1) {

                return null;
            }

            return this.entities[position];
        }

        @Override
        public long getItemId(int position) {

            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            View v;
            if (convertView != null
                    && convertView.getId() == R.id.SimpleListviewItemLayout) {

                v = convertView;

            } else {

                v = this.inflater.inflate(
                        R.layout.widget_listview_item_simple_layout, parent,
                        false);
            }

            TextView tv = (TextView)v.findViewById(R.id.ItemTextView);
            CategoryEntity e = this.entities[position];
            tv.setText(e.getName() + " " + e.getLimit());
            if (position == this.selectedIndex) {

                v.setBackgroundColor(Color.rgb(0, 100, 255));

            } else {

                v.setBackgroundColor(Color.BLACK);
            }

            return v;
        }

        public void setData(CategoryEntity[] entities) {

            if (entities == null) {

                return;
            }

            this.entities = new CategoryEntity[entities.length];
            for (int i = 0; i < entities.length; i++) {

                this.entities[i] = entities[i];
            }
        }
        public void setSelectedIndex(int index) {

            this.selectedIndex = index;
        }
    }
    private class OnMyViewClickListener implements View.OnClickListener {

        @Override
        public void onClick(View v) {

            if (v == ModifyCategoryActivity.this.ui.OperateButton) {

                ModifyCategoryActivity.this.PerformOperateButtonClick();
            }
        }
    }
    private class OnMyItemClickListener implements AdapterView.OnItemClickListener {

        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position,
                                long id) {

            if (parent == ModifyCategoryActivity.this.ui.CategorieslistView) {

                CategoryAdapter adapter = ((CategoryAdapter)parent.getAdapter());
                adapter.setSelectedIndex(position);
                adapter.notifyDataSetChanged();
            }
        }

    }
    private class OnMyItemLongClickListener implements AdapterView.OnItemLongClickListener {

        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view,
                                       int position, long id) {

            if (parent == ModifyCategoryActivity.this.ui.CategorieslistView) {

                CategoryAdapter adapter = ((CategoryAdapter)parent.getAdapter());
                adapter.setSelectedIndex(position);
                adapter.notifyDataSetChanged();
            }

            return false;
        }
    }
}

Here is the code of its layout ("app -> res -> layout -> activity_modify_category.xml"):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_modify_category"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FF000000"
    android:orientation="vertical"
    tools:context="com.casperlee.personalexpense.ModifyCategoryActivity">

    <LinearLayout
        android:id="@+id/layoutTop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <EditText
            android:id="@+id/CategoryEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ems="10"
            android:hint="@string/category_input_hint"
            android:theme="@android:style/Theme.Light"
            android:inputType="text" >

            <requestFocus />
        </EditText>

        <Button
            android:id="@+id/OperateButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/btn_add_caption2" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/layoutBottom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_below="@id/layoutTop">

        <ListView
            android:id="@+id/CategorieslistView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
        </ListView>
    </LinearLayout>

</RelativeLayout>

8. Add a new layout file named "widget_listview_item_simple_layout.xml" in the folder "app -> res -> layout", and put the following text in:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/SimpleListviewItemLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#FF000000"
    android:paddingLeft="3dp"
    android:paddingRight="3dp"
    android:paddingTop="9dp"
    android:paddingBottom="9dp"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/ItemTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="6dp"
        android:text=""
        android:textColor="@color/colorWhite"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</LinearLayout>

9. Add a new Activity class named "ModifySingleCategoryActivity" in the folder "app -> java -> com.casperlee.personalexpense", and put the following code in:

package com.casperlee.personalexpense;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.casperlee.personalexpense.dal.CategoryDal;
import com.casperlee.personalexpense.entities.CategoryEntity;

public class ModifySingleCategoryActivity extends AppCompatActivity {

    // Constants
    public static final String KEY_CATEGORY_ID = "CategoryID";
    public static final String KEY_CATEGORY_NAME = "CategoryName";
    public static final String KEY_CATEGORY_LIMIT = "CategoryLimit";

    // Fields
    private Layout ui;
    private int categoryID = -1;

    // Entrances
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_modify_single_category);

        this.ui = new Layout();

        this.readExtras();
        this.setListeners();
    }

    // Private functions
    private void readExtras() {

        categoryID = this.getIntent().getIntExtra(KEY_CATEGORY_ID, -1);
        ui.NameEditText.setText(this.getIntent().getStringExtra(KEY_CATEGORY_NAME));
        ui.LimitMoneyEditText.setText("" + this.getIntent().getDoubleExtra(KEY_CATEGORY_LIMIT, 0.0));
    }
    private void setListeners() {

        OnMyViewClickListener l = new OnMyViewClickListener();
        ui.OKButton.setOnClickListener(l);
    }
    private boolean readCategory(CategoryEntity e) {

        e.setId(this.categoryID);
        e.setName(ui.NameEditText.getText().toString().trim());
        if (e.getName().equals("")) {

            return false;
        }

        try {

            e.setLimit(Double.parseDouble(ui.LimitMoneyEditText.getText().toString()));

        } catch (NumberFormatException ex) {

            return false;
        }

        return true;
    }

    // Event handlers
    private void PerformOKButtonClick() {

        CategoryEntity e = new CategoryEntity();
        if (!this.readCategory(e)) {

            Toast.makeText(ModifySingleCategoryActivity.this, R.string.category_input_invalid, Toast.LENGTH_LONG).show();
            return;
        }

        CategoryDal.getInstance().update(e);
        this.finish();
    }

    // Classes
    private class Layout {

        public EditText NameEditText;
        public EditText LimitMoneyEditText;
        public Button OKButton;

        public Layout() {

            this.initialize();
        }

        private void initialize() {

            this.NameEditText = (EditText)ModifySingleCategoryActivity.this.findViewById(R.id.NameEditText);
            this.LimitMoneyEditText = (EditText)ModifySingleCategoryActivity.this.findViewById(R.id.LimitMoneyEditText);
            this.OKButton = (Button)ModifySingleCategoryActivity.this.findViewById(R.id.OKButton);
        }
    }
    private class OnMyViewClickListener implements View.OnClickListener {

        @Override
        public void onClick(View v) {

            if (v == ModifySingleCategoryActivity.this.ui.OKButton) {

                ModifySingleCategoryActivity.this.PerformOKButtonClick();
            }
        }
    }
}

Here is the code of its layout ("app -> res -> layout -> activity_modify_single_category.xml"):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_modify_single_category"
    android:orientation="vertical"
    android:background="#FF000000"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.casperlee.personalexpense.ModifySingleCategoryActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center">

        <TextView
            style="@style/LargeWhiteText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/caption_name" />

        <EditText
            android:id="@+id/NameEditText"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:inputType="text"
            android:theme="@android:style/Theme.Light"
            android:ems="10" >

            <requestFocus />
        </EditText>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center">

        <TextView
            style="@style/LargeWhiteText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/caption_limit" />

        <EditText
            android:id="@+id/LimitMoneyEditText"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:ems="10"
            android:theme="@android:style/Theme.Light"
            android:inputType="numberDecimal" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center"
        android:layout_marginTop="32dp">

        <Button
            android:id="@+id/OKButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btn_ok_caption"
            android:textAppearance="?android:attr/textAppearanceMedium"/>

    </LinearLayout>
</LinearLayout>

10. Open the file "app -> java -> com.casperlee.personalexpense -> AddExpenseActivity", add the following code into it:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        instance = this;
        ...
    }
    ...
    private static AddExpenseActivity instance;
    public static AddExpenseActivity getInstance() {

        return  instance;
    }
    ...
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        Intent intent;
        switch (item.getItemId()) {

            case R.id.miCategories:
                intent = new Intent();
                intent.setClass(AddExpenseActivity.this, ModifyCategoryActivity.class);
                intent.putExtra(ModifyCategoryActivity.KEY_INVOKE_TYPE, ModifyCategoryActivity.INVOKE_TYPE_MANAGE);
                this.startActivity(intent);
                break;
                ...
    }
    ...
    private void ModifyCategories() {

        Intent intent = new Intent();
        intent.setClass(AddExpenseActivity.this, ModifyCategoryActivity.class);
        intent.putExtra(ModifyCategoryActivity.KEY_INVOKE_TYPE, ModifyCategoryActivity.INVOKE_TYPE_ADD);
        this.startActivity(intent);
    }

11. Done!