Android之路——第一个上线 APP项目总结
引言
很多事经历之后或者失去之后才会懂得想去珍惜,也许你绕了一圈之后猛然回头才会发现原来她才是真爱……说来也惭愧,从Android1.6开始入门,大二之后所有课余时间都是在学Android,也算是很早就开始入门了,毕业前就想的是专心做Android 开发的工作,可是入职之后往往人在江湖身不由己……常常听到说他多么多么的喜欢android或者其他语言,可是很少一部分人都只是停留在口头上,一到面试也许总能说出一大堆idea,bla bla 的,可是真正努力去实现APP的少之又少,这样的喜欢是无人令人信服的。当然也许会有各种担心和顾忌,至今我大学时期参加谷歌大学学术合作的那个创新Android项目功能都还没能真正实现(目前市场上主流的app也还未实现,不知道是不能实现还是没兴趣实现?重拾Android也有一部分的原因是因为想要去实现这个未完成的梦),也想过,担心自己水平不够或者即使做出来了没有给自己带来附加利益什么的。可有些东西只有你去实践了你去动手了你才会真正体会到,也许在你是在你挑灯通宵攻克难关,也许是在你一次次Debugg中,也许就是在很平常的开发过程的那么一瞬间,突然你又有了新想法和新的理解。
一、APP简介
钟爱理财是一款理财记账app,包含支出明细信息、收入信息查询、扇形图统计消费评估、记账、入账、各月份支出、收入柱状图对比等多个功能,它能让你实现随时随地全天候记账、查账,完全的本地化存储数据,无需联网不消耗流量。如果感兴趣可以去360市场中搜索“钟爱理财”下载,互相学习学习。主界面效果图:
二、APP逐层分解总结
1. 引导界面
主要布局文件是自定义的继承自ViewGroup的滑动容器(实现ViewPager的效果),布局是把四个子布局文件嵌套到容器里,作为四个引导页面的布局,通过监听onTouchEvent事件实现滑动的效果,关键是计算移动的距离稍微复杂一点点。
1.1 自定义滑动容器的代码
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainRLayout" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#000000" > <cmo.fin.ui.UserScrollLayOut xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scrollLayout" android:layout_width="fill_parent" android:layout_height="fill_parent" android:visibility="visible" > <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/w01" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="396dp" android:text="@string/wel1_txt" android:textColor="#FFFFFF" android:textSize="16sp" /> RelativeLayout> <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/w02" > <TextView android:id="@+id/t1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="396dp" android:layout_marginBottom="20dp" android:text="@string/wel2_txt" android:textColor="#FFFFFF" android:textSize="16sp" /> RelativeLayout> <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/w03"> <TextView android:id="@+id/t2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="396dp" android:text="@string/wel3_txt" android:textColor="#FFFFFF" android:textSize="16sp" /> RelativeLayout> <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/wel_opendream" > <Button android:id="@+id/startBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_gravity="center_vertical" android:layout_marginBottom="90dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:background="@drawable/button_bg" android:text="开始你的筑梦之路" android:textColor="#FFFFFF" android:textSize="16sp" /> RelativeLayout> cmo.fin.ui.UserScrollLayOut> <LinearLayout android:id="@+id/llayout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="25dp" android:orientation="horizontal" android:visibility="visible" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:clickable="true" android:padding="5dp" android:src="@drawable/page_indicator_bg" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:clickable="true" android:padding="5dp" android:src="@drawable/page_indicator_bg" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:clickable="true" android:padding="5dp" android:src="@drawable/page_indicator_bg" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:clickable="true" android:padding="5dp" android:src="@drawable/page_indicator_bg" /> LinearLayout>RelativeLayout>
1.2 自定义滑动容器Java代码
package cmo.fin.ui;import cmo.fin.interfaces.IOnViewChangeListener;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewGroup;import android.widget.Scroller;/** * 类似ViewPager的效果 * @author cmo * @sumary 自定义布局容器 * @date 2016-06-13 * @version 新建 */public class UserScrollLayOut extends ViewGroup { private VelocityTracker mVelocityTracker; // private static final int SNAP_VELOCITY = 600; private Scroller mScroller; private int mCurScreen; private int mDefaultScreen = 0; private float mLastMotionX; private IOnViewChangeListener mOnViewChangeListener; public UserScrollLayOut(Context context) { super(context); init(context); } public UserScrollLayOut(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public UserScrollLayOut(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context) { mCurScreen = mDefaultScreen; mScroller = new Scroller(context); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (changed) { int childLeft = 0; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) { final int childWidth = childView.getMeasuredWidth(); childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int width = MeasureSpec.getSize(widthMeasureSpec); final int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); } scrollTo(mCurScreen * width, 0); } public void snapToDestination() { final int screenWidth = getWidth(); final int destScreen = (getScrollX() + screenWidth / 2) / screenWidth; snapToScreen(destScreen); } public void snapToScreen(int whichScreen) { // get the valid layout page whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1)); if (getScrollX() != (whichScreen * getWidth())) { final int delta = whichScreen * getWidth() - getScrollX(); mScroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 2); mCurScreen = whichScreen; invalidate(); // Redraw the layout if (mOnViewChangeListener != null) { mOnViewChangeListener.onViewChange(mCurScreen); } } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getAction(); final float x = event.getX(); switch (action) { case MotionEvent.ACTION_DOWN: if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(event); } if (!mScroller.isFinished()) { mScroller.abortAnimation(); } mLastMotionX = x; break; case MotionEvent.ACTION_MOVE: int deltaX = (int) (mLastMotionX - x); if (IsCanMove(deltaX)) { if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } mLastMotionX = x; scrollBy(deltaX, 0); } break; case MotionEvent.ACTION_UP: int velocityX = 0; if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); mVelocityTracker.computeCurrentVelocity(1000); velocityX = (int) mVelocityTracker.getXVelocity(); } if (velocityX > SNAP_VELOCITY && mCurScreen > 0) { // Fling enough to move left snapToScreen(mCurScreen - 1); } else if (velocityX < -SNAP_VELOCITY && mCurScreen < getChildCount() - 1) { // Fling enough to move right snapToScreen(mCurScreen + 1); } else { snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } break; } return true; } private boolean IsCanMove(int deltaX) { if (getScrollX() <= 0 && deltaX < 0) { return false; } if (getScrollX() >= (getChildCount() - 1) * getWidth() && deltaX > 0) { return false; } return true; } public void SetOnViewChangeListener(IOnViewChangeListener listener) { mOnViewChangeListener = listener; }}
2. 登录界面
布局文件就普通的EditText、Button、ImageView、CheckBox、TextView,当时一时没想到,应该使用自定义View的方式把ImageView和EditText结合起来,就不用总是用Relative去微调布局了。
3.主界面
布局主要是通过FragmentTabHost+Fragment+RadioButton实现。具体过程是这样的,利用RadioButton实现底部菜单栏,再注册RadioGroup的setOnCheckedChangeListener方法实现Tab之间的切换,用Framlayout存放Fragment也就是每一个Tab真正要展示的内容(貌似做还有更简单的方法可以实现,没有深究,至少比在Android2.X那段时间,实现这个用ViewPager和TabHost配合各种动画方便多了,更重要的是整个界面就一个MainActivity,真正的内容都是放到Fragment里的)
3.1 主界面布局
"http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > "@+id/realtabcontent" android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" /> "@+id/tab_rg_menu" android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@drawable/mmfooter_bg" android:orientation="horizontal" > "@+id/tab_rb_1" style="@style/tab_rb_style" android:checked="true" android:drawableTop="@drawable/tab_selector_pay" android:text="@string/tab_pay" /> "@+id/tab_rb_2" style="@style/tab_rb_style" android:drawableTop="@drawable/tab_selector_income" android:text="@string/tab_income" /> "@+id/tab_rb_3" style="@style/tab_rb_style" android:drawableTop="@drawable/tab_selector_faxian" android:text="@string/tab_evalate" /> "@+id/tab_rb_4" style="@style/tab_rb_style" android:drawableTop="@drawable/tab_selector_more" android:text="@string/tab_more" /> </RadioGroup> " android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" > @android :id/tabcontent" android:layout_width="0dp" android:layout_height="0dp" android:layout_weight="0" />
2.2 Fragment布局文件(我这里是展示ListView)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/activity_bcg" android:orientation="vertical" > <RelativeLayout android:layout_width="match_parent" android:layout_height="42dp" android:background="@drawable/search_selector_date" > <ImageView android:id="@+id/item_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@id/pay_itemname" android:layout_alignTop="@id/pay_itemname" android:layout_marginLeft="4dp" android:layout_marginTop="4dp" android:background="@drawable/manage_date_icon" /> <EditText android:id="@+id/pay_search_date" android:layout_width="wrap_content" android:layout_height="43dp" android:layout_alignParentTop="true" android:layout_toRightOf="@+id/item_icon" android:background="@drawable/search_selector_date" android:editable="false" android:ems="10" android:hint="@string/search_date_txt" android:inputType="none" android:paddingLeft="1dp" > <requestFocus /> EditText> <ImageButton android:id="@+id/search_btn" android:layout_width="90dp" android:layout_height="42dp" android:layout_alignParentRight="true" android:layout_toRightOf="@id/pay_content" android:background="@drawable/search_selector_date" /> <TextView android:id="@+id/pay_btn_txt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerHorizontal="false" android:layout_marginTop="5dp" android:layout_toRightOf="@id/pay_content" android:text="@string/search_btn_txt" android:textColor="#fff" android:textSize="20sp" android:textStyle="bold" /> RelativeLayout> <HorizontalScrollView android:layout_width="wrap_content" android:layout_height="wrap_content" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" > <TextView android:layout_width="120dp" android:layout_height="wrap_content" android:gravity="left" android:text="@string/pay_item_head" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#008001" android:textSize="24sp" android:textStyle="bold" /> <TextView android:layout_width="120dp" android:layout_height="wrap_content" android:gravity="left" android:text="@string/pay_money_head" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#f40000" android:textSize="24sp" android:textStyle="bold" /> <TextView android:layout_width="120dp" android:layout_height="wrap_content" android:gravity="left" android:text="@string/pay_date_head" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#e4f25d" android:textSize="24sp" android:textStyle="bold" /> <TextView android:layout_width="120dp" android:layout_height="wrap_content" android:gravity="left" android:text="@string/pay_type_head" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#df0f7b" android:textSize="24sp" android:textStyle="bold" /> <TextView android:layout_width="200dp" android:layout_height="wrap_content" android:gravity="left" android:text="@string/pay_remark_head" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#ccffc2" android:textSize="24sp" android:textStyle="bold" /> LinearLayout> HorizontalScrollView> <HorizontalScrollView android:layout_width="match_parent" android:layout_height="wrap_content" > <ListView android:id="@+id/pay_list" android:layout_width="wrap_content" android:layout_height="wrap_content" /> HorizontalScrollView>LinearLayout>
2.3 主界面的Java代码
package cmo.fin.ui;import cmo.fin.ui.PieChartFragment;import cmo.fin.ui.IncomeFragment;import cmo.fin.ui.MoreFragment;import cmo.fin.ui.PayoutFragment;import cmo.fin.ui.PayoutFragment.OnBackListener;import android.app.Activity;import android.os.Bundle;import android.support.v4.app.FragmentActivity;import android.support.v4.app.FragmentTabHost;import android.view.Menu;import android.widget.RadioGroup;import android.widget.Toast;import android.widget.RadioGroup.OnCheckedChangeListener;import android.widget.TabHost.TabSpec;/** * @author cmo * @sumary app主界面 * @date 2015-06-13 * @version 新建 */public class MainActivity extends FragmentActivity implements OnBackListener { // 定义FragmentTabHost对象 private FragmentTabHost mTabHost; private RadioGroup mTabRg; private final Class[] fragments = { PayoutFragment.class, IncomeFragment.class, PieChartFragment.class, MoreFragment.class }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost); mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent); // 得到fragment的个数 int count = fragments.length; for (int i = 0; i < count; i++) { // 为每一个Tab按钮设置图标、文字和内容 TabSpec tabSpec = mTabHost.newTabSpec(i + "").setIndicator(i + ""); // 将Tab按钮添加进Tab选项卡中 mTabHost.addTab(tabSpec, fragments[i], null); } mTabRg = (RadioGroup) findViewById(R.id.tab_rg_menu); mTabRg.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.tab_rb_1: mTabHost.setCurrentTab(0); break; case R.id.tab_rb_2: mTabHost.setCurrentTab(1); break; case R.id.tab_rb_3: mTabHost.setCurrentTab(2); break; case R.id.tab_rb_4: mTabHost.setCurrentTab(3); break; default: break; } } }); mTabHost.setCurrentTab(0); } @Override public void onBackPressed() { }}
4. 其他维护数据界面
主要维护数据的界面,都是简单的Activity,录入数据保存,做一些数据校验之后,保存到本地数据库中。
5. ListView展示
主要用于数据的展示,添加了垂直和水平方向的滚动条,ListView的实现主要就是继承BaseAdapter复写getCount(),getItemId(),getView()方法,其中因为ListView每一次刷新的时候都会调用getView,则每次都会先去查找View,所以设计了一个View缓存类 用于缓存查找出来的View,提高效率。
package cmo.fin.adapters;import java.util.List;import cmo.fin.entitys.MonthPay;import cmo.fin.ui.R;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.TextView;/** * @author cmo * @sumary MonthPay ListView的自定义适配器 * @date 2015-07-3 * @version 新建 */public class PayoutInfAdapter extends BaseAdapter { private Context mContext; private List mPayLst;//每个条目要绑定的数据集合 private int mViewId;//绑定的条目界面 private LayoutInflater mInflater;//使用xml文件生成对应的view对象 public PayoutInfAdapter(Context context,List pay, int mViewId) { //通过构造方法的方式传递过来 this.mContext=context; this.mPayLst = pay; this.mViewId = mViewId; this.mInflater=(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public int getCount() { // 得到要绑定的数据总数 return mPayLst.size(); } @Override public Object getItem(int position) { // 通过索引值获取对应的对象 return mPayLst.get(position); } @Override public long getItemId(int position) { // 获取索引值 return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { TextView nameView; TextView moneyView; TextView typeView; TextView dateView; TextView remarkView; ViewCache cache; // 初始化条目界面并实现数据的绑定 if(convertView==null){ convertView=mInflater.inflate(mViewId, null);//生成条目界面对象 nameView=(TextView)convertView.findViewById(R.id.item_name); dateView=(TextView)convertView.findViewById(R.id.item_date); moneyView=(TextView)convertView.findViewById(R.id.item_money); typeView=(TextView)convertView.findViewById(R.id.item_type); remarkView=(TextView)convertView.findViewById(R.id.item_remark); cache=new ViewCache(); cache.nameView=nameView; cache.dateView=dateView; cache.moneyView=moneyView; cache.typeView=typeView; cache.remarkView=remarkView; convertView.setTag(cache); } else{ cache=(ViewCache)convertView.getTag(); nameView=cache.nameView; dateView=cache.dateView; moneyView=cache.moneyView; typeView=cache.typeView; remarkView=cache.remarkView; } //绑定数据 MonthPay pay=mPayLst.get(position); nameView.setText(pay.getPayItemName()); dateView.setText(pay.getPayDate()); moneyView.setText(pay.getMoney()); typeView.setText(pay.getItemType()); remarkView.setText(pay.getPayContent()); //分别给Item里每个View注册长点击事件 /*cache.nameView.setOnLongClickListener(new TxtClickListener(position)); cache.moneyView.setOnLongClickListener(new TxtClickListener(position)); cache.dateView.setOnLongClickListener(new TxtClickListener(position)); cache.typeView.setOnLongClickListener(new TxtClickListener(position)); cache.remarkView.setOnLongClickListener(new TxtClickListener(position)); cache.nameView.setOnCreateContextMenuListener(null); */ return convertView; } //View缓存类 用于缓存查找出来的View,因为ListView每一次刷新的时候都会调用getView,则每次都会先去查找View private final class ViewCache{ public TextView nameView; public TextView dateView; public TextView moneyView; public TextView typeView; public TextView remarkView; }}
6. 图表展示界面
主要是动态扇形图统计和年度个月份柱状图对比
6.1扇形统计图
package cmo.fin.ui;import java.text.DecimalFormat;import java.text.NumberFormat;import java.util.Calendar;import java.util.List;import java.util.Random;import org.achartengine.ChartFactory;import org.achartengine.GraphicalView;import org.achartengine.model.CategorySeries;import org.achartengine.renderer.DefaultRenderer;import org.achartengine.renderer.SimpleSeriesRenderer;import cmo.fin.entitys.PayImfPie;import cmo.fin.services.IncomeDbOperate;import cmo.fin.services.ItemTypeDbOperate;import cmo.fin.services.PayDbOperate;import android.content.Context;import android.graphics.Color;import android.os.Bundle;import android.support.v4.app.Fragment;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.view.ViewGroup.LayoutParams;import android.widget.DatePicker;import android.widget.EditText;import android.widget.ImageButton;import android.widget.LinearLayout;import android.widget.TextView;/** * @author cmo * @sumary app消费统计饼形图 * @date 2015-07-13 * @version 新建 * @version 增加了查询栏和当月支出总额 */public class PieChartFragment extends Fragment { private String user;//当前的登录用户 private String title = "月支出统计分布图";// 饼图标题 private CategorySeries mSeries;// 饼图数据 private DefaultRenderer mRenderer;// 饼图描绘器 private GraphicalView mChartView;// 显示PieChart private Context context; private double data[]= new double[5];//对应显示的数据 //private LinearLayout mLinear;// 布局方式 private int[] COLORS = new int[] { Color.RED, Color.GREEN, Color.BLUE, Color.MAGENTA, Color.CYAN, Color.YELLOW, Color.DKGRAY };// 颜色 private double sumPayMoney = 0;// 总数 private SimpleSeriesRenderer renderer;// 饼图每块描绘器 private LinearLayout mPieLinearLayout; private TextView mSumIncome; private TextView mTotalPay; private TextView mBalence; private TextView mSumpay; private TextView mPlanpay; private ImageButton mSearchBtn; private EditText mSearchdateEdt; private ItemTypeDbOperate mTypeDbOperate; private IncomeDbOperate mIncomeDbOperate; private PayDbOperate mPayDbOperate; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View parentView = inflater.inflate(R.layout.fragment_paypie, container, false); //mPieLinearLayout=(LinearLayout)parentView.findViewById(R.id.chart); this.mTypeDbOperate = new ItemTypeDbOperate(parentView.getContext()); this.mIncomeDbOperate=new IncomeDbOperate(parentView.getContext()); this.mPayDbOperate=new PayDbOperate(parentView.getContext()); getViews(parentView); Bundle bundle=getActivity().getIntent().getExtras(); user=bundle.getString("LoginUser"); initPie(); intiImf(); bindListener(); return parentView; } @Override public void onResume() { refreshPie(); super.onResume(); } /* * 设置总余额、本月支出等信息 */ private void intiImf(){ String sumMonIncome=mIncomeDbOperate.getMonSumIncome(user,null); String sumIncome=mIncomeDbOperate.getSumIncome(user); String sumPay=mPayDbOperate.getSumPay(user); String sumMonPay=mPayDbOperate.getMonSumPay(user,null); //设置总收入 if(sumIncome!=null){ mSumIncome.setText("总收入: "+sumIncome+"元"); } if(sumPay!=null){ mTotalPay.setText("总支出:"+sumPay+"元"); } //设置总余额 if(sumIncome!=null && sumPay!=null){ Double balence= Double.valueOf(sumIncome)-Double.valueOf(sumPay); DecimalFormat nf=(DecimalFormat)NumberFormat.getInstance(); nf.setGroupingUsed(false); nf.setMaximumFractionDigits(2);//设置浮点数2位小数 mBalence.setText("总余额:"+String.valueOf(nf.format(balence))+"元"); } //设置本月支出 if(sumMonPay!=null){ mSumpay.setText("本月支出: "+String.valueOf(sumMonPay)+"元"); } if(sumMonPay=="0"){ title=""; } if(sumMonIncome!=null){ mPlanpay.setText("本月收入: "+sumMonIncome+"元"); } } private void getViews(View parentView){ mPieLinearLayout=(LinearLayout)parentView.findViewById(R.id.chart); //mPieLinearLayout.setBackgroundResource(R.drawable.activity_bcg); context=getActivity();//.getBaseContext(); mSumIncome=(TextView) parentView.findViewById(R.id.sumincome); mTotalPay=(TextView) parentView.findViewById(R.id.totalpay);//总支出 mBalence=(TextView) parentView.findViewById(R.id.sumbalance); mSumpay=(TextView) parentView.findViewById(R.id.sumpay);//本月支出 mPlanpay=(TextView) parentView.findViewById(R.id.planpay); mSearchBtn = (ImageButton) parentView.findViewById(R.id.search_btn); mSearchdateEdt = (EditText) parentView.findViewById(R.id.pay_search_date); } private void bindListener() { mSearchdateEdt.setOnClickListener(new DateEdtClickListener()); // mSearchdateEdt.setOnFocusChangeListener(new DateEdtFocusListener()); mSearchBtn.setOnClickListener(new SearchBtnClickListener()); } //初始化饼形图 private void initPie(){ mRenderer = new DefaultRenderer();// 创建一个描绘器的实例,将被用来创建图表 mRenderer.setZoomButtonsVisible(true);// 显示放大缩小功能按钮 mRenderer.setStartAngle(180);// 设置为水平开始 mRenderer.setDisplayValues(true);// 显示数据 // mRenderer.setFitLegend(false);// 设置是否显示图例 mRenderer.setLegendTextSize(24);// 设置图例字体大小 // mRenderer.setLegendHeight(10);// 设置图例高度 mRenderer.setShowLegend(false);// 默认是显示的下载需要关闭,因为动态更新数据的时候,图例更新慢 mRenderer.setChartTitle(title);// 设置饼图标题 mRenderer.setChartTitleTextSize(28);// 设置饼图标题大小 mRenderer.setLabelsTextSize(24); mSeries = new CategorySeries(""); sumPayMoney = 0; String date = mSearchdateEdt.getText().toString(); if (date.isEmpty()) { //date = "2011年4月"; Calendar now = Calendar.getInstance(); date = String.valueOf(now.get(Calendar.YEAR)) + "年"+ String.valueOf((now.get(Calendar.MONTH) + 1)) + "月"; } date=date.substring(0, 7); //获取后台的消费类别和对应的金额 List pieList = mTypeDbOperate.getPayTypeMoney(user,date); int pieNum=pieList.size(); if(pieNum>0){ data =new double[pieNum]; } for (int k = 0; k < pieNum; k++) { data[k] = Double.valueOf(pieList.get(k).getPercent());//每一份的具体数据大小 sumPayMoney += data[k];// 总的数据大小 } for (int i = 0; i < pieList.size(); i++) { mSeries.add(pieList.get(i).getType().toString()+"("+pieList.get(i).getPercent()+"元)", Double.valueOf(pieList.get(i).getPercent())/sumPayMoney);// 设置种类名称和对应的数值,前面是(key,value)键值对 renderer = new SimpleSeriesRenderer(); if (i < COLORS.length) { renderer.setColor(COLORS[i]);// 设置描绘器的颜色 } else { renderer.setColor(getRandomColor());// 设置描绘器的颜色 } renderer.setChartValuesFormat(NumberFormat.getPercentInstance());// 设置百分比 mRenderer.addSeriesRenderer(renderer);// 将最新的描绘器添加到DefaultRenderer中 } mChartView = ChartFactory.getPieChartView(context, mSeries, mRenderer);// 构建mChartView mPieLinearLayout.addView(mChartView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } // 分别产生RBG数值 public static int getRandomColor() { Random random = new Random(); int R = random.nextInt(255); int G = random.nextInt(255); int B = random.nextInt(255); return Color.rgb(R, G, B); } /************************************* 内部控件监听器类 Start **************************************************/ private final class DateEdtClickListener implements View.OnClickListener { @Override public void onClick(View v) { OpenDateTimeDialog(); } } private final class SearchBtnClickListener implements View.OnClickListener { @Override public void onClick(View v) { refreshPie(); } } /** * 打开日期选择对话框 */ private void OpenDateTimeDialog() { Calendar c = Calendar.getInstance(); // 最后一个false表示不显示日期,如果要显示日期,最后参数可以是true或者不用输入 new PickerDialogYearMon(getActivity(), 0, new PickerDialogYearMon.OnDateSetListener() { @Override public void onDateSet(DatePicker startDatePicker, int startYear, int startMonthOfYear, int startDayOfMonth, DatePicker endDatePicker, int endYear, int endMonthOfYear, int endDayOfMonth) { String textString = String.format("%d年%d月", startYear, startMonthOfYear + 1); mSearchdateEdt.setText(textString); } }, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c .get(Calendar.DATE), false).show(); } /** * 根据查询栏的日期值刷新Pie */ private void refreshPie() { String date = mSearchdateEdt.getText().toString(); if (date.isEmpty()) { Calendar now = Calendar.getInstance(); date = String.valueOf(now.get(Calendar.YEAR)) + "年"+ String.valueOf((now.get(Calendar.MONTH) + 1)) + "月"; } date=date.substring(0, 7); mSeries.clear(); sumPayMoney = 0; //获取后台的消费类别和对应的金额 List pieList = mTypeDbOperate.getPayTypeMoney(user,date); int pieNum=pieList.size(); if(pieNum>0){ data =new double[pieNum]; } for (int k = 0; k < pieNum; k++) { data[k] = Double.valueOf(pieList.get(k).getPercent());//每一份的具体数据大小 sumPayMoney += data[k];// 总的数据大小 } for (int i = 0; i < pieList.size(); i++) { mSeries.add(pieList.get(i).getType().toString()+"("+pieList.get(i).getPercent()+"元)", Double.valueOf(pieList.get(i).getPercent())/sumPayMoney);// 设置种类名称和对应的数值,前面是(key,value)键值对 renderer = new SimpleSeriesRenderer(); if (i < COLORS.length) { renderer.setColor(COLORS[i]);// 设置描绘器的颜色 } else { renderer.setColor(getRandomColor());// 设置描绘器的颜色 } renderer.setChartValuesFormat(NumberFormat.getPercentInstance());// 设置百分比 mRenderer.addSeriesRenderer(renderer);// 将最新的描绘器添加到DefaultRenderer中 } mChartView.repaint(); }}
6.2柱状统计图
package cmo.fin.ui;import java.util.Calendar;import org.achartengine.ChartFactory;import org.achartengine.GraphicalView;import org.achartengine.chart.BarChart.Type;import org.achartengine.model.CategorySeries;import org.achartengine.model.XYMultipleSeriesDataset;import org.achartengine.renderer.SimpleSeriesRenderer;import org.achartengine.renderer.XYMultipleSeriesRenderer;import cmo.fin.services.IncomeDbOperate;import android.app.Activity;import android.content.Context;import android.content.Intent;import android.graphics.Color;import android.graphics.Paint.Align;import android.os.Bundle;import android.view.View;import android.view.ViewGroup.LayoutParams;import android.widget.LinearLayout;/** * @author cmo * @sumary 各月份支出柱状图 * @version 新建 2015-07-24 */public class IncomeBarChartActivity extends Activity { private GraphicalView mChartView; private CategorySeries mCategorySeries ; private XYMultipleSeriesRenderer mRenderer ; private XYMultipleSeriesDataset mDataset; private SimpleSeriesRenderer mSeriesRenderer; private Context context; private LinearLayout mLinear; private String mlogUser = ""; private IncomeDbOperate mInDbOperate; private double maxY=9000;//初始值 public void back(View v) { Intent intent = new Intent(); intent.setClass(IncomeBarChartActivity.this, MainActivity.class); startActivity(intent); IncomeBarChartActivity.this.finish(); } protected void setChartSettings(XYMultipleSeriesRenderer renderer, String title, String xTitle, String yTitle, double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) { renderer.setChartTitle(title); renderer.setXTitle(xTitle); renderer.setYTitle(yTitle); renderer.setXAxisMin(xMin); renderer.setXAxisMax(xMax); renderer.setYAxisMin(yMin); renderer.setYAxisMax(yMax); renderer.setAxesColor(axesColor); renderer.setLabelsColor(labelsColor); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_incomebar); this.mInDbOperate=new IncomeDbOperate(getApplicationContext()); Bundle bundle = getIntent().getExtras(); mlogUser = bundle.getString("LoginUser"); context = getApplicationContext(); mLinear = (LinearLayout) findViewById(R.id.monincomebar); initBar(); } /** * 初始化各月份支出统计柱状图 */ private void initBar() { mRendererSetting(); Calendar now = Calendar.getInstance(); String year=now.get(Calendar.YEAR)+"年"; //year="2011年"; mCategorySeries = new CategorySeries(String.valueOf(now.get(Calendar.YEAR))); String monthPay=""; for (int k = 1; k < 13; k++) { monthPay=mInDbOperate.getTheMonSumIncomeMoney(mlogUser, year, k);//指定年度月份的消费总额 mCategorySeries.add(Double.valueOf(monthPay)); ///mCategorySeries.add(v[k]); if(Double.valueOf(monthPay)>maxY){ maxY=Double.valueOf(monthPay); } } mDataset = new XYMultipleSeriesDataset(); mDataset.addSeries(mCategorySeries.toXYSeries()); mRendererSetting(); mChartView = ChartFactory.getBarChartView(context, mDataset, mRenderer, Type.DEFAULT); mRenderer.setClickEnabled(true); mLinear.addView(mChartView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); mChartView.repaint(); } private void mRendererSetting() { mRenderer = new XYMultipleSeriesRenderer(); mRenderer.setAxisTitleTextSize(24); mRenderer.setChartTitleTextSize(28); mRenderer.setLabelsTextSize(24); mRenderer.setLabelsColor(Color.GREEN); mRenderer.setLegendTextSize(22); mSeriesRenderer = new SimpleSeriesRenderer(); mSeriesRenderer.setColor(PieChartFragment.getRandomColor()); mSeriesRenderer.setChartValuesTextSize(24); mSeriesRenderer.setDisplayChartValues(true); mRenderer.addSeriesRenderer(mSeriesRenderer); setChartSettings(mRenderer, "各月份收入柱状图", "月份", "收入金额(元)", 0.5, 12.5, 0, maxY, Color.GREEN, Color.RED); //mRenderer.setDisplayChartValues(true); mRenderer.setXLabels(12); mRenderer.setYLabels(10); mRenderer.setXLabelsAlign(Align.LEFT); mRenderer.setYLabelsAlign(Align.LEFT); mRenderer.setPanEnabled(true, false); mRenderer.setZoomEnabled(true); mRenderer.setZoomButtonsVisible(true); mRenderer.setZoomRate(1.1f); mRenderer.setBarSpacing(0.5f); } @Override public void onDestroy() { super.onDestroy(); }}
7. 数据库操作类和实体类
为了便于维护我把每一个表封装成对应的实体Bean,把一些通用的操作封装到了工具类。其中采用单例模式,封装了数据库类。
package cmo.fin.dbhelper;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteDatabase.CursorFactory;import android.database.sqlite.SQLiteOpenHelper;/** * @author cmo * @sumary 数据库相关操作类:新建数据库和各种数据表 * @date 2015-06-0 * @version 新建 */public final class DBOperateHelper extends SQLiteOpenHelper { private static final String dbName="FinancialDataBase.db"; private static DBOperateHelper mDBHelper; private final String createUserTb="Create Table SysUser(id integer primary key autoincrement,loginUserId varchar(16),sysUserName varchar(50),passWord varchar(16),phoneNumber varchar(13))";//创建用户表 private final String createMonthInComeTb="Create Table MonthIncome(id integer primary key autoincrement,incomeItemName varchar(50),money varchar(50),inComeContent varchar(200),userId varchar(16),incomeDate varchar(50),itemType varchar(50))"; private final String createMonthPayTb="Create Table MonthPay(id integer primary key autoincrement,payItemName varchar(50),money varchar(50),payContent varchar(200),userId varchar(16),payDate varchar(50),itemType varchar(50))"; private final String createInTypeTb="Create Table IncomeItemType(id integer primary key autoincrement,incomeTypeName varchar(50))"; private final String createPayTypeTb="Create Table PayItemType(id integer primary key autoincrement,payTypeName varchar(50))"; public DBOperateHelper(Context context){ super(context,dbName,null,1);//创建DB成功之后会保存在//databases/初始版本号为1 } public static DBOperateHelper getInstance(Context ctx) { if (mDBHelper == null) { //this will ensure no multiple instances out there. mDBHelper = new DBOperateHelper(ctx.getApplicationContext()); } return mDBHelper; } @Override public void onCreate(SQLiteDatabase db) { //在数据库第一次被创建时候,创建相关数据表 // TODO Auto-generated method stub try{ db.execSQL(createUserTb);//可以不指定字段的类型、长度,因为int类型也可以保存Char类型的 db.execSQL(createMonthInComeTb); db.execSQL(createMonthPayTb); //db.execSQL(createMonthPlanTb); db.execSQL(createInTypeTb); db.execSQL(createPayTypeTb); } catch(Exception e){ return; } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //数据库版本号升级时候触发,即version变化时 }}
其他业务相关操作类
package cmo.fin.services;import java.text.DecimalFormat;import java.text.NumberFormat;import java.util.ArrayList;import java.util.Calendar;import java.util.HashMap;import java.util.List;import android.content.Context;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;import cmo.fin.dbhelper.DBOperateHelper;import cmo.fin.entitys.MonthPay;import cmo.fin.entitys.PayImfPie;/** * MonthPay表的各种操作:查询、插入、更新、删除 * * @author cmo * @date 2015-06-27 * @version 新建 */public final class PayDbOperate { private DBOperateHelper dbHelper; public PayDbOperate(Context context) { // this.dbHelper=DBOperateHelper.getInstance(context); this.dbHelper = new DBOperateHelper(context); } /** * 插入新记录 * * @param monthPay * MonthPay 类的各种信息 */ public void insertRecord(MonthPay monthPay) { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.execSQL( "Insert into MonthPay(payItemName,money,payContent,userId,payDate,itemType) Values(?,?,?,?,?,?)", new Object[] { monthPay.getPayItemName(), monthPay.getMoney(), monthPay.getPayContent(), monthPay.getUserId(), monthPay.getPayDate(), monthPay.getItemType() }); db.close(); } /** * 删除记录 * * @param id */ public void deleRecordById(Integer id) { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.execSQL("Delete from MonthPay Where id=?", new Object[] { id }); db.close(); } /** * 更新记录 * * @param user * @param id */ public void updRecordById(MonthPay pay, String itemName) { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.execSQL( "Update MonthPay set money=?,payDate=?,itemType=?, Where payItemName=?", new Object[] { pay.getMoney(), pay.getPayDate(), pay.getItemType(), itemName }); db.close(); } /** * 分页获取记录 * * @param user * 当前登录的用户名 * @param yearMonth * 年月 * @param offset * 跳过前面多少记录 * @param maxRecord * 每页获取多少记录 * @return String user,String yearMonth, */ public List getScrollData(int offset, int maxRecord, String yearMonth, String loginUser) { yearMonth=yearMonth.substring(0, 7); List pays = new ArrayList(); DecimalFormat nf=(DecimalFormat)NumberFormat.getInstance(); nf.setGroupingUsed(false); nf.setMaximumFractionDigits(2);//设置浮点数2位小数 SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db .rawQuery( "Select * from MonthPay where substr(payDate,0,8)=? and userId=? order by id asc limit ?,?", new String[] { yearMonth, loginUser, String.valueOf(offset), String.valueOf(maxRecord) }); if (cursor != null) { while (cursor.moveToNext()) { String name = cursor.getString(cursor .getColumnIndex("payItemName")); String money = cursor.getString(cursor.getColumnIndex("money")); money=nf.format(Double.valueOf(money)); String date = cursor .getString(cursor.getColumnIndex("payDate")); String type = cursor.getString(cursor .getColumnIndex("itemType")); String remark = cursor.getString(cursor .getColumnIndex("payContent")); pays.add(new MonthPay(name, date, money, type, remark)); } cursor.close(); db.close(); } return pays; } /** * 获取所有的支出总额 * * @param user * @return */ public String getSumPay(String user) { String sum = "0"; DecimalFormat nf=(DecimalFormat)NumberFormat.getInstance(); nf.setGroupingUsed(false); nf.setMaximumFractionDigits(2);//设置浮点数2位小数 SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db.rawQuery( "Select sum(money) SumPay from MonthPay where userId=? ", new String[] { user }); if (cursor != null) { while (cursor.moveToNext()) { sum = cursor.getString(cursor.getColumnIndex("SumPay")); if(sum!=null){ sum=nf.format(Double.valueOf(sum)); } else{ sum="0"; } } cursor.close(); db.close(); } return sum; } /** * 获取所有的支出总额 * * @param user * @return */ public String getMonSumPay(String user, String yearMonth) { if (yearMonth == null) { Calendar now = Calendar.getInstance(); yearMonth = now.get(Calendar.YEAR) + "年" + (now.get(Calendar.MONTH) + 1) + "月"; // yearMonth="2011年4月"; } yearMonth=yearMonth.substring(0, 7); DecimalFormat nf=(DecimalFormat)NumberFormat.getInstance(); nf.setGroupingUsed(false); nf.setMaximumFractionDigits(2);//设置浮点数2位小数 String sum = "0"; SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db .rawQuery( "Select sum(money) SumMonPay from MonthPay where userId=? and substr(payDate,0,8)=? ", new String[] { user, yearMonth }); if (cursor != null) { while (cursor.moveToNext()) { sum = cursor.getString(cursor.getColumnIndex("SumMonPay")); if(sum!=null){ sum=nf.format(Double.valueOf(sum)); } else{ sum="0"; } } cursor.close(); db.close(); } return sum; } /** * 获取指定月份下的所有消费类别和对应类别下的所有话费小计 * * @param user * 当前登录用户名 * @param year * 当年年度 :如2011年 * @return */ public String getTheMonSumPayMoney(String user, String year, int month) { SQLiteDatabase db = dbHelper.getReadableDatabase(); String yearMon=year+String.valueOf(month)+"月";;//年度-月份:2011年4月 Cursor cursor=null; String subMoney = "0";// 各种类别对应的花费总额 DecimalFormat nf=(DecimalFormat)NumberFormat.getInstance(); nf.setGroupingUsed(false); nf.setMaximumFractionDigits(2);//设置浮点数2位小数 //1月到9月各月份的消费总额 if(month<10){ cursor = db.rawQuery("Select sum(money) subMoney from MonthPay where userId=? and substr(payDate,0,8)=? group by substr(payDate,0,8) order by substr(payDate,0,8) ", new String[] { user, yearMon }); } else{ cursor = db.rawQuery("Select sum(money) subMoney from MonthPay where userId=? and substr(payDate,0,9)=? group by substr(payDate,0,9) order by substr(payDate,0,9) ", new String[] { user, yearMon }); } if (cursor != null) { while (cursor.moveToNext()) { subMoney = cursor.getString(cursor.getColumnIndex("subMoney")); subMoney=nf.format(Double.valueOf(subMoney)); } cursor.close(); db.close(); } if(subMoney.length()==0){ subMoney = "0"; } return subMoney; }}
三、开发过程遇到的问题、经验教训总结
1. 引导界面
引导界面开发的过程中没有遇到多大的难题,唯一算是问题难点的话,应该就是每一次移动的距离一下子看不出来,需要去模拟计算一番。
2. 登录界面
登录界面也没有遇到多大难度的问题,主要是进行一些用户、密码的格式校验,注册各种事件并绑定之类的,记住用户信息,自动填充密码等等。本来想给用户信息加密之后再保存,但是后面想到的是本APP基本都是自己一个人使用,离线也是可以使用的,就没有加密,或许应该加密之后再保存。其他消费金额数据也是如此。
3. ListView界面
ListView界面效果实现的过程中没有什么难度,根据自定义的日期输入框输入日期作为查询条件,查询数据库,把返回信息封装成List<>返回再赋给ListView即可,唯一麻烦的就是微调细节,比如说一第一行的数据作为列表的标题栏,怎么与其他列区分?怎么才能更好的做到每一列的宽度自适应?还有一个问题是,不知道为何在Fragment定义了上下文菜单,无响应?总的来说目前的ListView界面效果采取了折中的原则,总算是实现出来了预期效果。如果下一版本优化的话我或许会再换另一个方案,或许采用HTML的方式展现数据,或许会更好些?
4. 图形引擎
此次采用的引擎是Android中很早就开始使用的achartengine,使用很简单,扇形图还OK,就是柱状图显示的时候,当每个月的数据都很大的时候,显示的数据都挤到了一起,这也算是目前存在的bug,下一个版本的时候我会换一个引擎,最近找到了一个MPAndroidChart是一款基于Android的开源图表库,MPAndroidChart不仅可以在Android设备上绘制各种统计图表,而且可以对图表进行拖动和缩放操作,应用起来非常灵活。
5. 整个APP的代码结构
由于初衷是单纯地为她做出一个APP,只供她一个人使用,没有过多的考虑到了后期维护和升级的问题,就只是简单地封装了一些代码,导致后期去修改一个功能或增加一个功能的时候,都要去修改源码,维护成本很高,等到有时间再把所有的源码,利用多态、单例模式、工厂模式、模板方法模式等重构一遍。
四、自己的一些建议和感受
- 所有的文件和变量都应命名规范,言之有意,特别是同类的资源看名字应该就能知道他的用途,比如说有两个xml文件,都是支出信息相关的,一个是activity的布局,另一个是Fragment的布局文件,那么你应该利用一个后缀区分,像系统的自动生成的xxx_activity,还比如说同样一个图片文件,一个是显示在EditText上的,一个是作为Button的背景,两个又是相似名字的也应该加以前缀或者后缀区分,这样会在你后期修改或者查找资源的时候带来很多便利
- 在开发初期就应该,考虑到了不同分辨率的适配性,尽量少用固定的位置和大小。
- 在开发初期都应该要考虑APP的国际化了,因为万一你想发布到google play 你就只需要换资源即可,不必修改源码
- 最后把所有的提示信息, 与代码分离,单独保存到string文件中,这样做也是为了日后你要发布其他语言版本的时候只需要添加上一个资源文件即可不比修改源码。
- 做好注释工作,随时保存,并做好版本管理,最好使用专业的版本管理工具,保留所有的版本
- 动手之前,先大致统筹全局 ,做一个简单的计划,把功能划分一二,把通用的一些封装成工具类,一些类似的操作可以采用多态利用设计模式进行编码。
更多相关文章
- 一句话锁定MySQL数据占用元凶
- android view statusBar 沉浸式
- Android中利用Handler在子线程中更新界面--简单的小球上下跳动案
- Android(安卓)打造炫目的圆形菜单 秒秒钟高仿建行圆形菜单
- Android中使用WebView与JS交互全解析
- Android基于IIS的APK下载(三)用JSON传输更新数据
- 友盟2013年上半年数据报告:与开发者相关的各种干货数据
- 强悍的跨平台开源多媒体中心XBMC介绍
- 为什么Android应用用Java开发,为什么Android大型游戏要用数据包?这