Android开发之安卓上Mac样式的按钮

 更新时间:2016年9月20日 19:57  点击:1701
对于初学者对于app中的样式与按钮可能不知道如何来做了,下文我们就一起来看一篇Android开发之安卓上Mac样式的按钮了,例子如下。

最近试着玩玩Android开发,做一个小玩意儿的时候,总感觉默认的按钮样式太糟糕,看到网上几幅IPhone截图,觉得按钮有点感觉,就想着抄一个过来……相当的没有技术含量,只不过记性不好,记录一下。

PhotoShop上的准备

用简单的方法,设置background图片。

先渐变填充圆角矩形(半径4左右),渐变的首尾颜色自己定好了,不过过渡位置我试了下,大约是上图1所示,在40%和60%增加两个色标,值为首尾之差的1/3和2/3。
做出立体效果和阴影,如2所示
做出边框线,如3所示
然后调两个亮一点和暗一点的(没用过IPhone不知道有焦点和按下时什么样子我这里就偷懒弄个明暗变化)按钮。
应用到按钮上

在drawable里定义一个式样文件

<?xml version="1.0" encoding="utf-8"?>
<selector
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_pressed="true" android:drawable="@drawable/game_button_pressed" />
  <item android:state_focused="true" android:drawable="@drawable/game_button_focused" />
  <item android:drawable="@drawable/game_button_normal" />
</selector>

然后按钮的background指向它,这就完了~~

追记:

后来看到一种完全用XML定义按钮的方法,靠谱,虽然变化好像有些单调,但可以不用图片毕竟是好事,先记录一下方法,有空试试~


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:state_pressed="true">
  <shape>
   <gradient android:startColor="#0d76e1" android:endColor="#0d76e1" android:angle="270" />
   <stroke android:width="1dip" android:color="#f403c9" />
   <corners android:radius="2dp" />
   <padding android:left="10dp" android:top="10dp"
    android:right="10dp" android:bottom="10dp" />
  </shape>
 </item>
 
 <item android:state_focused="true">
  <shape>
   <gradient android:startColor="#ffc2b7" android:endColor="#ffc2b7" android:angle="270" />
   <stroke android:width="1dip" android:color="#f403c9" />
   <corners android:radius="2dp" />
   <padding android:left="10dp" android:top="10dp"
    android:right="10dp" android:bottom="10dp" />
  </shape>
 </item>
 
 <item>
  <shape>
   <gradient android:startColor="#000000" android:endColor="#ffffff" android:angle="180" />
   <stroke android:width="1dip" android:color="#f403c9" />
   <corners android:radius="5dip" />
   <padding android:left="10dp" android:top="10dp"
    android:right="10dp" android:bottom="10dp" />
  </shape>
 </item>
</selector>

appBase是一个Android app开发的基础集合,这样的目的可以让我们的Android app开发得更快速,本文我们来看看用appBase如何快速开发购物车

appBase基础介绍

appBase是什么?

appBase是一个Android app开发的基础集合,目的是任何应用都可以在这个基础之上开发app,省去了搭建框架的时间。

appBase=xutils+fastjson+avlib

    xutils使用了其中HttpUtils、BitmapUtils、DbUtils
    fastjson使用了json解析
    avlib大家比较陌生,这个库是我另外一个简单的工具库。主要功能是View的自动绑定、View的常用数据自动绑定、万能Adapter等

目的:是为了让懂java的同学能够快速上手Android开发。


一、看看框架结构

01.jpeg

- apicloud.sdk是对apicloud的云API的调用做了简单封装
- base:只包含BaseActivity
- http:基于HttpUtils简化了常用的网络请求,定义网络参数APIs的配置
- presenter:采用了MVP中的P来命名,可以让非UI处理业务抽出放到这个结构中,因此BasePresenter诞生了。
- util:常用的工具类
- widget:常用的自定义组件(待扩展)
- Application:继承android.app.Application,为了统一使用框架中的组件对象,避免了组件的重复创建。因此建议使用这个类配置application的name。当然也可以基于此类扩展。


二、创建一个新项目

    第一步:创建一个空的Android project
    技术分享
    注意:删除自动添加的android-support-v4.jar(appBase中包含有)
    第二步:引用appBase
    技术分享

    第三步:修改AndroidManifest.xml

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.snicesoft.appbase.demo"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />

    <application
        android:allowBackup="true"
        android:name="com.snicesoft.Application"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
    </application>
    </manifest>

    添加:android:name=”com.snicesoft.Application”

    第四步:创建Activity

    package com.snicesoft.appbase.demo;
    import com.snicesoft.avlib.annotation.Layout;
    import com.snicesoft.avlib.rule.IData;
    import com.snicesoft.avlib.rule.IHolder;
    import com.snicesoft.base.BaseActivity;
    @Layout(R.layout.activity_main)
    public class MainActivity extends
        BaseActivity<MainActivity.Holder, MainActivity.Data> {
        public class Holder extends IHolder {

            @Override
            public void initViewParams() {

            }

        }

        public class Data extends IData {

        }

        @Override
        public Data newData() {
            return new Data();
        }

        @Override
        public Holder newHolder() {
            return new Holder();
        }
    }

    看着class一栏,大家可能会花了眼,怎么这么长。这只是一种写法,推荐的写法(内部类)。我来说明下这个类:
    IHolder是指View自动绑定的容器
    IData是指View的数据自动绑定容器

    第五步:使用IHolder和IData

    package com.snicesoft.appbase.demo;
    import com.snicesoft.avlib.annotation.DataBind;
    import com.snicesoft.avlib.annotation.Id;
    import com.snicesoft.avlib.annotation.Layout;
    import com.snicesoft.avlib.rule.IData;
    import com.snicesoft.avlib.rule.IHolder;
    import com.snicesoft.base.BaseActivity;
    @Layout(R.layout.activity_main)
    public class MainActivity extends
        BaseActivity<MainActivity.Holder, MainActivity.Data> {
        public class Holder extends IHolder {
            @Id(R.id.textView1)
            TextView textView1;
            @Id(R.id.button1)
            Button button1;
            @Override
            public void initViewParams() {

            }

        }

        public class Data extends IData {
            @DataBind(id = R.id.textView1)
            String tv1 = "我是自动绑定的TextView";
            @DataBind(id = R.id.button1)
            String btn1 = "我是自动绑定的Button";
        }

        @Override
        public Data newData() {
            return new Data();
        }

        @Override
        public Holder newHolder() {
            return new Holder();
        }
    }

    运行结果

02.png


Android appBase 购物车开发


购物车,在商城app中是必不可少的一部分,也是使用的比较多的,这里简单的做一个效果。

先来看看效果图

这里写图片描述


1、创建项目

第一种、引用appBase项目即可
第二种、将appBase的jar文件copy到libs下

03.jpeg

我用的第二种,如上图所示。


2、代码生成

通过代码生成器生成Activity、Presenter、Adapter

1、生成Activity(默认生成Presenter)

01.jpeg
2、生成Adapter

01.jpeg
3、网络请求数据

这里网络数据使用的是APICloud,那么就需要对于APICloudSDK进行配置。最新的appBase讲配置放了出来,只要在applcation中进行代码配置就可以了。


package com.example.shopcartdemo;

import com.apicloud.sdk.APICloudSDK;
import com.snicesoft.Application;
import com.snicesoft.http.HttpReq;

public class MyApplication extends Application {
    final String APP_ID = "A6960031839242";
    final String APP_KEY = "3F248D5F-50DB-782A-F437-E13796238B9E";

    @Override
    public void onCreate() {
        super.onCreate();
        APICloudSDK.getInstance().init(APP_ID, APP_KEY);
        APICloudSDK.getInstance().init(hu());
        HttpReq.debug = true;
    }
}


这里添加了DialogPresenter,作用就是为了请求的时候对dialog的控制

DialogPresenter


package com.example.shopcartdemo.presenter;

import android.app.ProgressDialog;
import android.content.Context;

import com.snicesoft.presenter.BasePresenter;
import com.snicesoft.util.DialogUtil;

public class DialogPresenter<C extends BasePresenter.Callback> extends
        BasePresenter<C> {

    public DialogPresenter(Context context) {
        super(context);
        progressDialog = DialogUtil.getProgressDialog(context);
    }

    ProgressDialog progressDialog;

    protected void showDialog(CharSequence message, boolean... flag) {
        if (flag != null) {
            if (flag.length > 0)
                progressDialog.setCancelable(flag[0]);
            if (flag.length > 1)
                progressDialog.setCanceledOnTouchOutside(flag[1]);
        }
        progressDialog.setMessage(message);
        if (!progressDialog.isShowing())
            progressDialog.show();
    }

    protected void closeDialog() {
        if (progressDialog.isShowing())
            progressDialog.dismiss();
    }
}


MainPresenter


package com.example.shopcartdemo.presenter;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;

import com.alibaba.fastjson.JSON;
import com.apicloud.sdk.APICloudSDK;
import com.example.shopcartdemo.adapter.ShopCartAdapter;
import com.lidroid.xutils.exception.HttpException;
import com.snicesoft.http.HttpCallback;
import com.snicesoft.presenter.BasePresenter;
import com.snicesoft.util.CommonUtils;

public class MainPresenter extends DialogPresenter<MainPresenter.Callback> {

    public MainPresenter(Context context) {
        super(context);
    }

    public interface Callback extends BasePresenter.Callback {
        void setShopCartList(List<ShopCartAdapter.Data> list);
    }

    public static class ShopCart {
        String title;
        int price;
        int count;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }

    }

    public void getShopCartList() {
        showDialog("正在加载");
        APICloudSDK
                .getInstance()
                .GET("/mcm/api/ShopCart?filter=%7B%22where%22%3A%7B%7D%2C%22skip%22%3A0%2C%22limit%22%3A20%7D",
                        null, new HttpCallback() {

                            @Override
                            public void onSuccess(String result) {
                                closeDialog();
                                List<ShopCart> array = JSON.parseArray(result,
                                        ShopCart.class);
                                List<ShopCartAdapter.Data> list = new ArrayList<ShopCartAdapter.Data>();
                                for (ShopCart cart : array) {
                                    list.add(new ShopCartAdapter.Data(
                                            0,
                                            cart.title,
                                            cart.price,
                                            "http://ck.haier.com/UpLoad/2015-05-15/a5e8cac4-2671-4aa0-83a7-66c64e051f95.jpg",
                                            cart.count));
                                }
                                if (callback != null)
                                    callback.setShopCartList(list);
                            }

                            @Override
                            public void onFailure(HttpException arg0) {
                                closeDialog();
                                CommonUtils.showToast(getContext(), "请求失败,请稍后重试");
                            }
                        });
    }
}



在这个类中,网络请求和网络解析实体对象都用内部类来定义,注意:内部类定义一定要用static class,否则fastjson无法正常解析,会导致无法反射创建对象。

4、数据绑定和交互

首先看下Activity


package com.example.shopcartdemo;

import java.util.List;

import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.TextView;

import com.example.shopcartdemo.adapter.ShopCartAdapter;
import com.example.shopcartdemo.adapter.ShopCartAdapter.ViewCallback;
import com.example.shopcartdemo.presenter.MainPresenter;
import com.snicesoft.avlib.annotation.DataBind;
import com.snicesoft.avlib.annotation.DataType;
import com.snicesoft.avlib.annotation.Id;
import com.snicesoft.avlib.annotation.Layout;
import com.snicesoft.avlib.rule.IData;
import com.snicesoft.avlib.rule.IHolder;
import com.snicesoft.base.BaseActivity;

@Layout(R.layout.activity_main)
public class MainActivity extends
        BaseActivity<MainActivity.Holder, MainActivity.Data> implements
        MainPresenter.Callback, ViewCallback {
    class Holder extends IHolder {
        @Id(R.id.lvShopCard)
        ListView lvShopCard;
        @Id(R.id.cbAll)
        CheckBox cbAll;
        @Id(R.id.tvPrice)
        TextView tvPrice;

        @Override
        public void initViewParams() {
            cbAll.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    _data.shopCartAdapter.setAll(cbAll.isChecked());
                    tvPrice.setText("¥" + _data.shopCartAdapter.calc());
                }
            });
        }
    }

    class Data extends IData {
        @DataBind(id = R.id.lvShopCard, dataType = DataType.ADAPTER)
        ShopCartAdapter shopCartAdapter = new ShopCartAdapter(getBaseContext());
    }

    @Override
    public Holder newHolder() {
        return new Holder();
    }

    @Override
    public Data newData() {
        return new Data();
    }

    MainPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenter = new MainPresenter(this);
        presenter.setCallback(this);
        _data.shopCartAdapter.setCallback(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        presenter.getShopCartList();
    }

    @Override
    public void onClick(View v) {
        super.onClick(v);
        switch (v.getId()) {
        case R.id.btnGoPay:
            break;
        case R.id.btnDelete:
            break;
        default:
            break;
        }
    }

    boolean isNormal = true;

    @Override
    public void setShopCartList(List<ShopCartAdapter.Data> list) {
        _data.shopCartAdapter.setDataList(list);
        refreshView();
    }

    @Override
    public void refreshView() {
        _holder.cbAll.setChecked(_data.shopCartAdapter.isAll());
        _holder.tvPrice.setText("¥" + _data.shopCartAdapter.calc());
    }

}


基本的Holder和Data将组件和数据进行简单的管理,清晰可见。
presenter将业务进行分离,将传统的activity中请求数据进行分离。
ViewCallback这个类是为了解决Adapter与Activity直接的交互定义的接口。

接下来看看Adapter


package com.example.shopcartdemo.adapter;

import android.content.Context;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageView;

import com.example.shopcartdemo.R;
import com.snicesoft.avlib.AVLib;
import com.snicesoft.avlib.annotation.DataBind;
import com.snicesoft.avlib.annotation.DataType;
import com.snicesoft.avlib.annotation.Id;
import com.snicesoft.avlib.annotation.Layout;
import com.snicesoft.avlib.rule.IData;
import com.snicesoft.avlib.rule.IHolder;
import com.snicesoft.avlib.view.ViewFinder;
import com.snicesoft.avlib.widget.AvAdapter;

@Layout(R.layout.item_shopcart)
public class ShopCartAdapter extends
        AvAdapter<ShopCartAdapter.Holder, ShopCartAdapter.Data> {

    public ShopCartAdapter(Context context) {
        super(context);
    }

    class Holder extends IHolder {
        @Id(R.id.btnAdd)
        Button btnAdd;
        @Id(R.id.btnDelete)
        Button btnDelete;
        @Id(R.id.cbSelect)
        CheckBox cbSelect;
        @Id(R.id.img)
        ImageView img;

        @Override
        public void initViewParams() {
        }
    }

    public static class Data extends IData {
        long gid;

        boolean isChecked = true;

        public long getGid() {
            return gid;
        }

        @DataBind(id = R.id.tvTitle)
        String title;
        @DataBind(id = R.id.tvPrice, prefix = "¥")
        int price;
        @DataBind(id = R.id.img, dataType = DataType.IMG)
        String image;
        @DataBind(id = R.id.edtCount)
        int count;

        public Data(long gid, String title, int price, String image, int count) {
            super();
            this.gid = gid;
            this.title = title;
            this.price = price;
            this.image = image;
            this.count = count;
        }
    }

    public interface ViewCallback {
        void refreshView();
    }

    ViewCallback callback;

    public void setCallback(ViewCallback callback) {
        this.callback = callback;
    }

    @Override
    public Holder newHolder() {
        return new Holder();
    }

    @Override
    public void bindAfter(int position, final View view, Holder holder,
            final Data data) {
        holder.cbSelect.setOnCheckedChangeListener(null);
        holder.cbSelect.setChecked(data.isChecked);
        holder.btnAdd.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                data.count++;
                AVLib.dataBindTo(data, new ViewFinder(view), "count");
                if (callback != null)
                    callback.refreshView();
            }
        });
        holder.btnDelete.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if (data.count > 1) {
                    data.count--;
                    AVLib.dataBindTo(data, new ViewFinder(view), "count");
                    if (callback != null)
                        callback.refreshView();
                }
            }
        });

        holder.cbSelect
                .setOnCheckedChangeListener(new OnCheckedChangeListener() {

                    @Override
                    public void onCheckedChanged(CompoundButton buttonView,
                            boolean isChecked) {
                        data.isChecked = isChecked;
                        if (callback != null)
                            callback.refreshView();
                    }
                });

    }

    public void setAll(boolean isChecked) {
        for (Data data : getDataList()) {
            data.isChecked = isChecked;
        }
        notifyDataSetChanged();
    }

    public int calc() {
        int total = 0;
        for (Data data : getDataList()) {
            if (data.isChecked)
                total += data.count * data.price;
        }
        return total;
    }

    public boolean isAll() {
        for (Data data : getDataList()) {
            if (!data.isChecked)
                return false;
        }
        return true;
    }
}



Holder的组件定义原则:需要在当前所在类中调用即可定义
Data的DataBind使用原则:需要绑定的的组件都可以。
当然通过上面可以看出Data中可以定义不用DataBind注解的字段,这个替代了传统的辅助Map或者List解决一些问题。
在上面的Data中用isChecked来表示CheckBox是否选中。这个问题,在我刚开始写android的时候,常常会用Map集合将position对应的boolean值记录下来,然后在getView中去检测。现在可以简单的通过Data几种管理,使得Adapter的字段不需要那么繁琐。

对于isChecked的使用产生了下面3个业务方法
setAll:这个方法用来设置全部选中(Activity中的全选按钮事件)
calc:这个方法用来计算购物车总额(Activity中的总计)
isAll:这个方法用来判断是否全部被选中(Activity的全选按钮设置checked的依据)


开发思路我们就讲得差不多了,做开发,最重要的还是思路,整体代码我们就不给出了,有兴趣的朋友可以自己整理代码,加深印象。

现在的app应用都可以调用手机的摄像头了,这样可以直接拍照了,对于在开发端来讲摄像头调用有许多的一些细节要注意了,下面来看一篇小编整理的Android应用调用摄像头开发例子


这两天玩Android玩的废寝忘食,Blog都好几天没加东西了,惭愧!记录一下这两天最崩溃的一个问题。

好早就装了开发环境,真正着手还是这两天,非常的生疏,虽然有SDK文档,那么多蚊子一般的字,实在没心思慢慢研究。这不想调用摄像头,原以为很容易就能搞定的,累计花了大概有一天的时间才只能保证不出错……至于效果嘛,难说啊!

先看API-examples里有调用 摄像头的例子,在模拟器上虽然看不出什么效果,毕竟还是能执行的,就是一个方块在黑白相间的背景上移动呗。

就这么一个Google提供的范例,传到我的HTC G2上也能一执行就报错,我对Google的尊敬之情顿时减少了0.0001%啊……(当然有可能是G2不够标准,但毕竟其他的软件都是能用的,看来是有不少健壮代码了啊)。联机调试看了一下,出错的这一行(android-7里的):


parameters.setPreviewSize(w, h);

查一下,摄像头不是所有随便的(w, h)都能够认识的,所以呢,我们有了下面这样的增强版:

List<Size> mSupportedPreviewSizes;
mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width, height);
 
private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio = (double) w / h;
        if (sizes == null) return null;
 
        Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;
 
        int targetHeight = h;
 
        // Try to find an size match aspect ratio and size
        for (Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }
 
        // Cannot find the one match the aspect ratio, ignore the requirement
        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }
        return optimalSize;
    }
后来的Sample里有了这段代码,看起来强大了不少。然而非常不幸的,首先getSupportedPreviewSizes()这个函数在2.1之后才有,我一开始是打算用1.6开发的……好吧我改,这个先不说,自己的手机已经刷到2.1了,这个函数的返回值居然是null?!如果确实想老版本上也用的话,怎么办??

有鉴于有软件可以达成,所以肯定是有方法的!得这么写:

public class SupportedSizesReflect {
 private static Method Parameters_getSupportedPreviewSizes = null; 
 private static Method Parameters_getSupportedPictureSizes = null;
 
 static {
  initCompatibility();
 };
 
 private static void initCompatibility() {
  try {
   Parameters_getSupportedPreviewSizes = Camera.Parameters.class.getMethod(
     "getSupportedPreviewSizes", new Class[] {});
 
   Parameters_getSupportedPictureSizes = Camera.Parameters.class.getMethod(
     "getSupportedPictureSizes", new Class[] {});
 
  } catch (NoSuchMethodException nsme) {
   nsme.printStackTrace();
   Parameters_getSupportedPreviewSizes = Parameters_getSupportedPictureSizes = null;   
  }  
 }
 
 /**
  * Android 2.1之后有效
  * @param p
  * @return Android1.x返回null
  */
 public static List<Size> getSupportedPreviewSizes(Camera.Parameters p) {
  return getSupportedSizes(p, Parameters_getSupportedPreviewSizes);
 }
 
 public static List<Size> getSupportedPictureSizes(Camera.Parameters p){
  return getSupportedSizes(p, Parameters_getSupportedPictureSizes);
 } 
 
 @SuppressWarnings("unchecked")
 private static List<Size> getSupportedSizes(Camera.Parameters p, Method method){
  try {
   if (method != null) {
    return (List<Size>) method.invoke(p);
   } else {
    return null;
   }
  } catch (InvocationTargetException ite) {
   Throwable cause = ite.getCause();
   if (cause instanceof RuntimeException) {
    throw (RuntimeException) cause;
   } else if (cause instanceof Error) {
    throw (Error) cause;
   } else {
    throw new RuntimeException(ite);
   }
  } catch (IllegalAccessException ie) {
   return null;
  }
 } 
}

啊啊~,リフレクションなんか、大嫌い……然后还要用类似这样的方法调用~


@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) {
 
 Camera.Parameters params = camera.getParameters();
 
 List<Size> supportedPictureSizes
    = SupportedSizesReflect.getSupportedPictureSizes(params);
 List<Size> supportedPreviewSizes
    = SupportedSizesReflect.getSupportedPreviewSizes(params);
 
 if ( supportedPictureSizes != null &&
  supportedPreviewSizes != null &&
  supportedPictureSizes.size() > 0 &&
  supportedPreviewSizes.size() > 0) {
 
  //2.x
  pictureSize = supportedPictureSizes.get(0);
 
  int maxSize = 1280;
  if(maxSize > 0){
   for(Size size : supportedPictureSizes){       
    if(maxSize >= Math.max(size.width,size.height)){
     pictureSize = size;
     break;
    }      
   }
  }
 
  WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
  Display display = windowManager.getDefaultDisplay();  
  DisplayMetrics displayMetrics = new DisplayMetrics();
  display.getMetrics(displayMetrics);
 
  previewSize = getOptimalPreviewSize(
       supportedPreviewSizes,
       display.getWidth(),
       display.getHeight());
 
  params.setPictureSize(pictureSize.width, pictureSize.height);  
  params.setPreviewSize(previewSize.width, previewSize.height);        
 
 }
 this.camera.setParameters(params);
 try {
  this.camera.setPreviewDisplay(holder);
 } catch (IOException e) {
  e.printStackTrace();
 }
 this.camera.startPreview();
}

死机无数次之后总结出来的啊,发现程序写的一个不好强制结束了,摄像头都无法再次启用了,kill都不行,只能重新启动手机才好。重启一次还那么慢,谁知道有比较适合G2的row?

哦还有一个,预览画面90°的,2.X后可以用parameters.set(“rotation”, “90″),之前的话得写成parameters.set(“orientation”, “portrait”)。但是据说不是所有的机器都可以的…


上面讲的是摄像头的初始化,如果觉得这么就万事OK的话,那就大错特错了。接下来的东西让人感到更加头痛。

在我的这个应用里,不需要把拍下来的图片存储,只需要把预览的图片数据处理一下就好,很自然的我只是用了onPreviewFrame调用,考虑处理传递进来的data数据流就是了。

网上很多帖子都说,然后用BitmapFactory的decodeByteArray()函数来解析图片就行了,我试了一下,发现这真是彻头彻尾的谎言,data字节流默认是YCbCr_420_SP(虽然可以改,但其他的格式未必兼容),decodeByteArray()压根儿不认!SDK2.2之后,似乎提供了一个YuvImage的类来转一下(那Google一开始提供这个借口是做什么的?),难道就要把老机给抛弃了么??万万不能啊(穷人最理解穷人们了)!

好在这个世界总是不缺少好人和牛人的,有人提供了这么一段转换的代码:

static public void decodeYUV420SP(int[] rgb, byte[] yuv420sp, int width, int height) {
    final int frameSize = width * height;
 
    for (int j = 0, yp = 0; j < height; j++) {
        int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
        for (int i = 0; i < width; i++, yp++) {
            int y = (0xff & ((int) yuv420sp[yp])) - 16;
            if (y < 0) y = 0;
            if ((i & 1) == 0) {
                v = (0xff & yuv420sp[uvp++]) - 128;
                u = (0xff & yuv420sp[uvp++]) - 128;
            }
 
            int y1192 = 1192 * y;
            int r = (y1192 + 1634 * v);
            int g = (y1192 - 833 * v - 400 * u);
            int b = (y1192 + 2066 * u);
 
            if (r < 0) r = 0; else if (r > 262143) r = 262143;
            if (g < 0) g = 0; else if (g > 262143) g = 262143;
            if (b < 0) b = 0; else if (b > 262143) b = 262143;
 
            rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
        }
    }
}

我不是很清楚这里面的原理,但是它能在我这里工作,暂时可以了……然后你才可以吧处理完的rgb[]传给decodeByteArray()。

顺便好心的把使用SDK2.2之后的也贴上吧,万一有用呢……

public void onPreviewFrame(byte[] data, Camera arg1) {
    FileOutputStream outStream = null;
    try {
        YuvImage yuvimage = new YuvImage(data,ImageFormat.NV21,arg1.getParameters().getPreviewSize().width,arg1.getParameters().getPreviewSize().height,null);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        yuvimage.compressToJpeg(new Rect(0,0,arg1.getParameters().getPreviewSize().width,arg1.getParameters().getPreviewSize().height), 80, baos);
 
        outStream = new FileOutputStream(String.format("/sdcard/%d.jpg", System.currentTimeMillis()));       
        outStream.write(baos.toByteArray());
        outStream.close();
 
        Log.d(TAG, "onPreviewFrame - wrote bytes: " + data.length);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
    }
    Preview.this.invalidate();
}


哦,得到的图像旋转了90°(似乎有的机型设置一下setRotation(90)可以搞定,但还是那句话,不通用啊,况且这个是2.1之后的API)。手动转一下吧……


Matrix matrix = new Matrix();
matrix.postRotate(90);
// 这里的rgb就是刚刚转换处理的东东
Bitmap bmp = Bitmap.createBitmap(rgb, 0, w, w, h, Bitmap.Config.ARGB_4444);
Bitmap nbmp = Bitmap.createBitmap(bmp,
          0, 0, bmp.getWidth(),  bmp.getHeight(), matrix, true);
终于正常了~~~

考虑到需要做识别,自然得先把它转成灰度图像,经典心理公式Gray = R*0.299 + G*0.587 + B*0.114出场了,但是手机的计算速度不那么快,这样的浮点运算还是尽量避免吧~ 于是考虑Gray = (R*299 + G*587 + B*114 + 500) / 1000或者Gray = (R*30 + G*59 + B*11 + 50) / 100。但是除法总是还是不够快,用移位吧……Gray = (R*19595 + G*38469 + B*7472) >> 16,稍微小一点,用Gray = (R*38 + G*75 + B*15) >> 7也足够了。

经过一番努力学习,把写就的代码兴致勃勃的在手机上跑了一下,虽然不够快结果出来了,想想也是大负荷运算啊,自我安慰客户应该可以有这样的耐心吧。

就在这个时候,我突然想起一件很重要的事情!
我需要的是灰度图,也就是亮度风量,而最开始的YUV,不就是亮度色度饱和度么?!那么Y分类不就是我需要的灰度值吗!!我在做什么,辛辛苦苦转成RGB,再转成亮度,吃饱了撑着不是。想到这里我立刻用头撞墙九九一百八十一次,一悼念我那白白死去的脑细胞的在天之灵。立刻重写,删除大量代码,快多了,效果也好~~ 鄙视一下两小时前的自己!

Android帧动画的结束事件对于做android开发的朋友非常的有用了,下面我们来看看Android帧动画的结束事件的例子。


最近在一个Android应用中,用到了帧动画。这东西的具体介绍就不讲了,网上到处是(虽然基本都是抄来抄去……)。用起来很简单效果也很好,不过这一次我有一个特殊的要求,希望帧动画在播放完毕的时候做一些其他的事情。

在渐变动画中,我们可以很简单的使用监听器:

XXX.setAnimationListener(new AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    }
    @Override
    public void onAnimationRepeat(Animation animation) {
    }
    @Override
    public void onAnimationEnd(Animation animation) {
        //渐变动画结束....
    }
});

然而帧动画却没有这样方便的功能,为什么??

当然,我们自己写的动画,自己应该是知道它会什么时候结束,启动动画的时候顺便启动一个线程就好了,说说简单做起来毕竟也是要花时间的啊,咱自己写一个类吧,以后就好重复用了,共享出来,要是谁也有这样的要求就直接用吧!

public abstract class MyAnimationDrawable extends AnimationDrawable {
    Handler finishHandler;      // 判断结束的Handler
    public MyAnimationDrawable(AnimationDrawable ad) {
        // 这里得自己把每一帧加进去
        for (int i = 0; i < ad.getNumberOfFrames(); i++) {
            this.addFrame(ad.getFrame(i), ad.getDuration(i));
        }
    }
    @Override
    public void start() {
        super.start();
        /**
         * 首先用父类的start()
         * 然后启动线程,来调用onAnimationEnd()
         */
        finishHandler = new Handler();
        finishHandler.postDelayed(
            new Runnable() {
                public void run() {
                    onAnimationEnd();
                }
            }, getTotalDuration());
    }
    /**
     * 这个方法获得动画的持续时间(之后调用onAnimationEnd())
     */
    public int getTotalDuration() {
        int durationTime = 0;
        for (int i = 0; i < this.getNumberOfFrames(); i++) {
            durationTime += this.getDuration(i);
        }
        return durationTime;
    }
    /**
     * 结束时调用的方法,一定要实现
     */
    abstract void onAnimationEnd();
}
 
/******************************************
 ***************** 使用方法 *****************
 ******************************************/
 
// 新建我们的类的实例
MyAnimationDrawable mad = new MyAnimationDrawable(
    (AnimationDrawable) getResources().getDrawable(R.drawable.anim1)) {
        @Override
        void onAnimationEnd() {
            // 实现这个方法,结束后会调用
        }
    };
// 把这个动画“赐福”给某个ImageView
iv.setBackgroundDrawable(mad);
// 开始吧
mad.start();
但是这个方法未必好,为什么呢,因为我们没法保证动画会完整流畅的播放完毕,也许因为其他的事情终止了,也不知到机器卡的时候会不会使动画播放时间变长,仅用在要求不高的情况下,至少,我这里是够了。要是有更好方法一定告知~

在安卓开发应用中Live Wallpaper设定中加入AdMob广告是一个常事了,我们在下载的app中都会有广告了,这些广告是维护app收入来源的一个不错的方式了。

自从Android 2.1中加入了动态壁纸,一下子牛叉了很多啊,漂亮的壁纸层出不穷,看上去老土的Android手机总算也是可以炫一下了。

动态壁纸基础

制作方法,网上太多了,虽然基本都是抄的,其实都是从sample上发展出来的,我也把要点记一下,每次写新的都要把老的工程打开看,啥记性……

res/xml中指定动态壁纸的xml文件:

<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
 android:settingsActivity="xxx.yyy.LiveWallpaperSettings"
 android:thumbnail="@drawable/icon" />

这里是说明自己图标和设定Activity。
还有一个设定Activity的xml文件,就是普通的PreferenceScreen,省略。

创建WallpaperService的子类,需要再onCreateEngine方法中返回一个Engine,Engine中画画儿,就像用SurfaceView一样。


public class LiveWallpaper extends WallpaperService {
 public static final String SHARED_PREFS_NAME = "setting_file_name";
 @Override
 public void onCreate() {
  super.onCreate();
 }
 @Override
 public void onDestroy() {
  super.onDestroy();
 }
 @Override
 public Engine onCreateEngine() {
  return new TestPatternEngine();
 }
 class TestPatternEngine extends Engine implements
   SharedPreferences.OnSharedPreferenceChangeListener {
  private final Handler mHandler = new Handler();
  private final Runnable mDrawPattern = new Runnable() {
   public void run() {
    drawFrame();
   }
  };
  private boolean mVisible;
  private SharedPreferences mPreferences;
  TestPatternEngine() {
   mPreferences = LiveWallpaper.this.getSharedPreferences(
     SHARED_PREFS_NAME, 0);
   mPreferences.registerOnSharedPreferenceChangeListener(this);
   onSharedPreferenceChanged(mPreferences, null);
  }
  public void onSharedPreferenceChanged(SharedPreferences prefs,
    String key) {
  }
  @Override
  public void onCreate(SurfaceHolder surfaceHolder) {
   super.onCreate(surfaceHolder);
  }
  @Override
  public void onDestroy() {
   super.onDestroy();
   mHandler.removeCallbacks(mDrawPattern);
  }
  @Override
  public void onVisibilityChanged(boolean visible) {
   mVisible = visible;
   if (visible) {
    drawFrame();
   } else {
    mHandler.removeCallbacks(mDrawPattern);
   }
  }
  @Override
  public void onSurfaceChanged(SurfaceHolder holder, int format,
    int width, int height) {
   super.onSurfaceChanged(holder, format, width, height);
   drawFrame();
  }
  @Override
  public void onSurfaceCreated(SurfaceHolder holder) {
   super.onSurfaceCreated(holder);
  }
  @Override
  public void onSurfaceDestroyed(SurfaceHolder holder) {
   super.onSurfaceDestroyed(holder);
   mVisible = false;
   mHandler.removeCallbacks(mDrawPattern);
  }
  @Override
  public void onOffsetsChanged(float xOffset, float yOffset, float xStep,
    float yStep, int xPixels, int yPixels) {
   drawFrame();
  }
  void drawFrame() {
   final SurfaceHolder holder = getSurfaceHolder();
   Canvas c = null;
   try {
    c = holder.lockCanvas();
    if (c != null) {
     // draw something
    }
   } finally {
    if (c != null)
     holder.unlockCanvasAndPost(c);
   }
   mHandler.removeCallbacks(mDrawPattern);
   if (mVisible) {
    mHandler.postDelayed(mDrawPattern, 1000 / 25);
   }
  }
 }
}

还有设定Activity的实现:

public class LiveWallpaperSettings extends PreferenceActivity
    implements SharedPreferences.OnSharedPreferenceChangeListener {
    @Override
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        getPreferenceManager().setSharedPreferencesName(LiveWallpaper.SHARED_PREFS_NAME);
        addPreferencesFromResource(R.xml.livewallpaper_settings);
        getPreferenceManager().getSharedPreferences()
            .registerOnSharedPreferenceChangeListener(this);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
    }
 
    @Override
    protected void onDestroy() {
        getPreferenceManager().getSharedPreferences()
            .unregisterOnSharedPreferenceChangeListener(this);
        super.onDestroy();
    }
 
    public void onSharedPreferenceChanged(
        SharedPreferences sharedPreferences, String key) {
    }
}

androidmanifest.xml中一定要加入的:


<service android:name=".LiveWallpaper" android:label="@string/app_name"  android:icon="@drawable/icon">
    <intent-filter>
        <action android:name="android.service.wallpaper.WallpaperService" />
    </intent-filter>
    <meta-data android:name="android.service.wallpaper" android:resource="@xml/livewallpaper" />
</service>
 
<activity android:label="@string/livewallpaper_settings"
   android:name=".LiveWallpaperSettings"
   android:theme="@android:style/Theme.Light.WallpaperSettings"
   android:exported="true"
   android:icon="@drawable/icon">
</activity>
 
<uses-sdk android:minSdkVersion="7" />
<uses-feature android:name="android.software.live_wallpaper" />

“uses-feature android:name=”android.software.live_wallpaper””这句话要慎用啊!Google的电子市场会认这句话,然后把有些可以用的机器过滤掉,比如俺的破机器,本来是不支持的,升级到2.1按说是可以用的。我看了市场上很多有名的动态壁纸,就没有加这句话~~
另外,如果想在动态壁纸中使用OpenGL ES,请参考这篇文章:Android中使用OpenGL ES的一二事。

加入AdMob广告

上面都是废话,我这次想说的主题是如何在壁纸设定界面里加入AdMob广告,好不容易做好的东西,总是要意思意思是吧,加个广告是没法的事情了。

不同于一般的Activity,直接加入AdMob是不行的,连个Layout都没有,而且直接加到壁纸上则非常糟糕,谁也不希望好好的背景上塞一个广告。所以一般动态壁纸的广告都加在设置界面里,这样还真有些不容易。

一般有两种方法,把Admob的adView当做一个Preference,直接加入xml就好。


public class AdmobPreference extends Preference {
 
    public AdmobPreference(Context context) {
        super(context, null);
    }
 
    public AdmobPreference(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    @Override
    protected View onCreateView(ViewGroup parent) {
        //override here to return the admob ad instead of a regular preference display
        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        return inflater.inflate(R.layout.admob_preference, null);
    }
 
}

相对的admob_preference的配置:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/ad_layout"
    android:layout_width="fill_parent" android:layout_height="fill_parent">
 
    <com.google.ads.AdView
     xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"
  android:id="@+id/ad"
  android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        ads.adSize="BANNER"
        ads:adUnitId="a14e51ca560d266"
        ads:loadAdOnCreate="true"
         />
 
</LinearLayout>
Android AdView missing required XML attribute "adsize"

这里还是有些学问的,网上查一下会有无数的“AdView missing required XML attribute ‘adSize’”的问题,据说是4.1.0之后带来的问题,一定要这么写(将”http://schemas.android.com/apk/lib/com.google.ads”当做一个命名空间,而不是加入自己的程序中)才能不出错,至少大家是如此说的,可惜我不行,怎么写都有这个错,耗费了两个小时尝试了各种写法,还是不行啊!!最后还是写在代码里了:(

不过光这么写貌似还是不行,AdView周围还有留白,貌似是PreferenceActivity里的padding,去不掉,最后还是使用了Tab的方式:


public class SettingsTabActivity extends TabActivity {
 
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.table_layout);
 
        TabHost tabHost = getTabHost();  // The activity TabHost
        TabHost.TabSpec spec;  // Resusable TabSpec for each tab
        Intent intent;  // Reusable Intent for each tab
 
        // Create an Intent for the regular live wallpaper preferences activity
        intent = new Intent().setClass(this, LivePaperSettings.class);
 
        // Initialize a TabSpec and set the intent
        spec = tabHost.newTabSpec("TabTitle").setContent(intent);
        spec.setIndicator("TabTitle");
 
        tabHost.addTab(spec);
 
        tabHost.setCurrentTab(0);   
    }

对应的layout:

 

<?xml version="1.0" encoding="utf-8"?>
 
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"
 android:id="@android:id/tabhost" android:layout_width="fill_parent"
 android:layout_height="fill_parent">
 
 <LinearLayout android:orientation="vertical"
  android:id="@+id/main_layout"
  android:layout_width="fill_parent" android:layout_height="fill_parent">
 
  <com.google.ads.AdView android:id="@+id/ad"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   ads:backgroundColor="#000000"
   ads:primaryTextColor="#FFFFFF"
   ads:secondaryTextColor="#CCCCCC"
   ads.adSize="BANNER"
   ads:adUnitId="a14e51ca560d266"
   ads:loadAdOnCreate="true" />
 
  <TabWidget android:id="@android:id/tabs"
   android:layout_width="fill_parent" android:layout_height="1dp"
   android:visibility="invisible" />
 
  <FrameLayout android:id="@android:id/tabcontent"
   android:layout_width="fill_parent" android:layout_height="fill_parent"
   android:padding="1dp" />
 </LinearLayout>
</TabHost>
大概就是如此了。

对了还有一个浪费了我两个多小时的问题,对于已经按照的live wallpaper,你修改了setting的Activity,直接覆盖安装是没效果的,点配置还是出现以前的Activity,发现这一点之前,我做了不知几百次修改上传,都快整挂了,Google怎么考虑的?!

[!--infotagslink--]

相关文章

  • 苹果告别“高大上”,越来越向安卓和中国用户靠近

    “一起,让我们将这个世界变得更好。”苹果首席执行官蒂姆 库克对着台下5000多名开发者说道,声音略有些沙哑和颤抖。...2016-07-04
  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • 利用JS实现点击按钮后图片自动切换的简单方法

    下面小编就为大家带来一篇利用JS实现点击按钮后图片自动切换的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2016-10-25
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • Android WebView加载html5页面实例教程

    如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
  • 深入理解Android中View和ViewGroup

    深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
  • Android自定义WebView网络视频播放控件例子

    下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
  • Android用MemoryFile文件类读写进行性能优化

    java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
  • 安卓开发之设置密码只能输入字母和数字的组合

    设置登录密码我们一般会有限制的如由什么组合了,下面我们来看一篇关于安卓开发之设置密码只能输入字母和数字的组合方法,具体的细节如下所示。 无论是电脑还是手机...2016-09-20
  • Android设置TextView竖着显示实例

    TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • BootStrap栅格系统、表单样式与按钮样式源码解析

    这篇文章主要为大家详细解析了BootStrap栅格系统、表单样式与按钮样式源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2017-01-23
  • Android 实现钉钉自动打卡功能

    这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
  • Android 开发之布局细节对比:RTL模式

    下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02
  • 自动设置安卓手机wifi代理的PowerShell脚本

    这篇文章主要介绍了自动设置安卓手机wifi代理的PowerShell脚本,帮助大家进行抓包测试,感兴趣的朋友可以了解下...2020-10-17
  • Android中使用SDcard进行文件的读取方法

    首先如果要在程序中使用sdcard进行存储,我们必须要在AndroidManifset.xml文件进行下面的权限设置: 在AndroidManifest.xml中加入访问SDCard的权限如下: <!--...2016-09-20
  • Android开发之PhoneGap打包及错误解决办法

    下面来给各位简单的介绍一下关于Android开发之PhoneGap打包及错误解决办法,希望碰到此类问题的同学可进入参考一下哦。 在我安装、配置好PhoneGap项目的所有依赖...2016-09-20