适用于所有设备的 Phonegap 通用签名捕获

Posted

技术标签:

【中文标题】适用于所有设备的 Phonegap 通用签名捕获【英文标题】:Phonegap universal signature capture for all devices 【发布时间】:2014-02-08 19:15:00 【问题描述】:

我想完成以下事情:

使用触摸屏设备获取签名,签名看起来不错,并且在用户签名时屏幕会响应,并且可以跨平台工作。

埃米尔

【问题讨论】:

【参考方案1】:

经过 2 周的挖掘、编码、测试、失败、阅读和重试,我已经设法将一些东西放在一起:

可能的解决方案:

    每个平台都有本机应用代码,但您有多个应用而不是一个 使用canvas,差不多就好了,只是在android上有2个问题: Android 浏览器倾向于跳过相当多的触摸事件,因此在画布中绘制签名时看起来很糟糕 在运行 2.3 及更低版本的设备上,您可能会遇到将画布转换为 base64 的困难,因为不支持该功能

我对此的解决方案是: - 使用 PhoneGap,并在 Android 上使用本机签名捕获活动,在所有其他设备上使用画布。事实证明,说起来容易做起来难。经过一番努力,我终于把它放在一起,这就是我想与大家分享的内容,这只是骨干,因此您可以在自己的项目中随意实现它。

首先,您需要安装 phonegap,然后创建一个新项目。您将需要以下 2 个插件:InAppBrowser 和 Device,请参阅文档如何获取。

创建项目后,为您的 index.html 使用以下代码(注意 jq.js 是 jquery 的本地副本,因此应用可以离线运行):

<html>
<head>
        <title>Simple Signature Capture</title>
</head>
<body>
    <div>
    <input type=button value='GET SIGNATURE' id=sign>
    </div>
</body>
</html> 

    <script type="text/javascript" src="phonegap.js"></script>
    <script type="text/javascript" src="jq.js"></script>
    <script type="text/javascript">
     var signatrue="";
        $("#sign").click(function()

        if (device.platform!=='Android') 
        // Use canvas method to get signature if device is different than Android
        var ref = window.open('sign.html', '_blank', 'location=no');
        ref.addEventListener('loaderror', function(event)  ref.close(); signatrue=event.url; print_picture(););
                    //As you see force the error listener and we assign the URL value to our variable
        
        // Use native android activity to get signature
        else 
                    // call to native function which starts the new intent
        window.HelloWorld.customFunctionCalled();
                    // function which will be called by the intent on result
        function got_signature()
                    // call to native function which will give us a variable which contains the signature in base4 format
        signature=window.HelloWorld.send_picture();
        print_picture();
        
        
        );
function print_picture()

alert(signatrue);

</script>

现在我们需要一个名为 sign.html 的文件,如果我们不在 Android 上,它将获取我们的签名,这是代码(注意 jq.js 是 jQuery 的同一个本地副本):

    <style>
    .container, html, body
    height:100%;
    width:100%;
    overflow:none;
    padding:0;
    margin:0;
    
    input[type=button] 
    width:50%;
    height:auto;
    text-align:center;
    font-size:18pt;
    float:left;
    
    .sign_place
    
    width:100%;
    clear:both;
    height:auto;
    
    </style>
    <script type="text/javascript" src="jq.js"></script>
    <html>
    <div class=container>
    <input type=button id=clear value='Clear'><input type=button id=save value='Save'>
    <div class="sign_place"></div>
    </div>
    </html>
    <script>
    //reset the canvas
    $("#clear").click(function()canvas.width = canvas.width;);
    // save the canvas and pass back the value to the parent - very dirty and cheap to force and error page so we can catch the string in the main window. 
    //data:image/png;base64, beginning must be removed otherwise browser will display the freshly captured image
    $("#save").click(function()var pngUrl = canvas.toDataURL().replace("data:image/png;base64,",""); window.location.href=pngUrl; );
    width=$("html").width()-10;
    height=$("html").height()-$("#clear").height()-10;
    $(".sign_place").html("<canvas id='signpad'   style='padding:0pxmargin:0px;border:0px solid black;'>Sorry, your browser is not supported.</canvas>");
    var canvas = document.getElementById('signpad');
    var context = canvas.getContext('2d');
        // create a drawer which tracks touch movements
        var drawer = 
            isDrawing: false,
            touchstart: function (coors) 
            context.beginPath();
            context.moveTo(coors.x, coors.y);
            this.isDrawing = true;
            ,
            touchmove: function (coors) 
                if (this.isDrawing) 
                    context.lineTo(coors.x, coors.y);
                    context.lineJoin = 'round';
                    context.lineWidth=5;
                    // adjust the lineWidth to look good. 
                    context.stroke();

                
            ,
            touchend: function (coors) 
                if (this.isDrawing) 
                    this.touchmove(coors);
                    this.isDrawing = false;
                
            
        ;
        // create a function to pass touch events and coordinates to drawer
        function draw(event)  
            var type = null;

            var coors;
            if(event.type === "touchend") 
                coors = 
                    x: event.changedTouches[0].pageX,
                    y: event.changedTouches[0].pageY
                ;
            
            else 
                // get the touch coordinates
                coors = 
                    x: event.touches[0].pageX,
                    y: event.touches[0].pageY
                ;
            
            type = type || event.type
            // pass the coordinates to the appropriate handler
            drawer[type](coors);
        
        // Listen for touch events and draw
            document.addEventListener('touchstart', draw, false);
            document.addEventListener('touchmove', draw, false);
            document.addEventListener('touchend', draw, false);        
        // prevent elastic scrolling
        document.body.addEventListener('touchmove', function (event) 
            event.preventDefault();
        , false); // end body.onTouchMove
        // Redo the canvas on orientation change
         window.addEventListener('orientationchange', function()
         // new to reduce max width and height otherwise canvas will be larger than the screen :(
         width=$("html").width()-10;
         height=$("html").height()-$("#clear").height()-10;
         // do not use CSS to adjust it it will stretch the signature and it will look unrealistic.
         $("#signpad").attr('width',width);
         $("#signpad").attr('height',height);

         );

    </script>

一切都好,几乎可以开始了,在你这样做之前,确保你将当前 jQuery 的副本保存到你的 www 文件夹中 jq.js 的名称下 使用 phonegap 为所需平台(ios、Android、Windows Phone、BB)构建应用程序。

构建完成后,进入 Android 构建并将主活动 java 修改为以下内容:

public class HelloWorld extends CordovaActivity 

//We will need this global string to pass the signature from an activity to our phonegap app
String signature="";

@Override
public void onCreate(Bundle savedInstanceState)

    super.onCreate(savedInstanceState);
    super.init();
    // Enable the Java <-> Javascript interaction
    appView.addJavascriptInterface(this, "HelloWorld");
    // Set by <content src="index.html" /> in config.xml
    super.loadUrl(Config.getStartUrl());
    //super.loadUrl("file:///android_asset/www/sign.html");


//@JavascriptInterface annotation must be used for compatibility with Android 4.2 and above
@JavascriptInterface
public void customFunctionCalled() 
 Intent intent = new Intent(HelloWorld.this, SignatureCapture.class);
 startActivityForResult(intent,1);

  
  @JavascriptInterface
   public String send_picture() 
        return signature;
  //This function only sends back the signature which is already is Base64 to the Javascript.
     
  @JavascriptInterface
   protected void onActivityResult(int requestCode, int resultCode, Intent data) 

      if (requestCode == 1) 

         if(resultCode == RESULT_OK)    
          signature = data.getStringExtra("RESULT_STRING");
          // Dirty trick to send Javascript command to PhoneGap
          // The trick will call the function got_signature() in Javascript
          // Which than will call the Java function get_picture
          // PhoneGap will NOT work if we try to send the image here with
          // something like super.sendJavascript("got_signature(var signature="+signature+");");
          // so do not even try to do that, it took me couple of hours
          // till I realized that there is a bug somewhere in phonegap
            super.sendJavascript("got_signature();");
       
         if (resultCode == RESULT_CANCELED)     
             //Write your code if there's no result
         
      
    

创建一个名为 SignatureCapture 的新类,确保您也在 mainfest 文件中定义它:

public class SignatureCapture extends Activity 
 LinearLayout mContent;
    signature mSignature;
    Button mClear, mGetSign, mCancel;

    public int count = 1;
    public String current = null;
    private Bitmap mBitmap;
    View mView;


    private String uniqueId;
    String ba1="";
    //private EditText yourName;

    @Override
    public void onCreate(Bundle savedInstanceState) 
    
        super.onCreate(savedInstanceState);
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.signature);


        //prepareDirectory();
        uniqueId = getTodaysDate() + "_" + getCurrentTime() + "_" + Math.random();
        current = uniqueId + ".png";
        //mypath= new File(directory,current);


        mContent = (LinearLayout) findViewById(R.id.linearLayout);
        mSignature = new signature(this, null);
        mSignature.setBackgroundColor(Color.WHITE);
        mContent.addView(mSignature, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
        mClear = (Button)findViewById(R.id.clear);
        mGetSign = (Button)findViewById(R.id.getsign);
        mGetSign.setEnabled(false);

        mView = mContent;



        mClear.setOnClickListener(new OnClickListener() 
                
            public void onClick(View v) 
            
                Log.v("log_tag", "Panel Cleared");
                mSignature.clear();
                mGetSign.setEnabled(false);
            
        );

        mGetSign.setOnClickListener(new OnClickListener() 
                
            public void onClick(View v) 
            
                Log.v("log_tag", "Panel Saved");
                boolean error = captureSignature();
                if(!error)
                    mView.setDrawingCacheEnabled(true);
                    mSignature.save(mView);
                    Bundle b = new Bundle();
                    b.putString("status", "done");
                    Intent intent = new Intent();
                    intent.putExtras(b);
                    intent.putExtra("RESULT_STRING",ba1);
                    setResult(RESULT_OK,intent);   
                    finish();
                
            
        );



    

    @Override
    protected void onDestroy() 
        Log.w("GetSignature", "onDestory");
        super.onDestroy();
    

    private boolean captureSignature() 

        boolean error = false;
        String errorMessage = "";


 //       if(yourName.getText().toString().equalsIgnoreCase(""))
 //           errorMessage = errorMessage + "Please enter your Name\n";
 //           error = true;
 //          

        if(error)
            Toast toast = Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT);
            toast.setGravity(Gravity.TOP, 105, 50);
            toast.show();
        

        return error;
    

    private String getTodaysDate()  

        final Calendar c = Calendar.getInstance();
        int todaysDate =     (c.get(Calendar.YEAR) * 10000) + 
        ((c.get(Calendar.MONTH) + 1) * 100) + 
        (c.get(Calendar.DAY_OF_MONTH));
        Log.w("DATE:",String.valueOf(todaysDate));
        return(String.valueOf(todaysDate));

    

    private String getCurrentTime() 

        final Calendar c = Calendar.getInstance();
        int currentTime =     (c.get(Calendar.HOUR_OF_DAY) * 10000) + 
        (c.get(Calendar.MINUTE) * 100) + 
        (c.get(Calendar.SECOND));
        Log.w("TIME:",String.valueOf(currentTime));
        return(String.valueOf(currentTime));

    



    public class signature extends View 
    
        private static final float STROKE_WIDTH = 5f;
        private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;
        private Paint paint = new Paint();
        private Path path = new Path();

        private float lastTouchX;
        private float lastTouchY;
        private final RectF dirtyRect = new RectF();

        public signature(Context context, AttributeSet attrs) 
        
            super(context, attrs);
            paint.setAntiAlias(true);
            paint.setColor(Color.BLACK);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeJoin(Paint.Join.ROUND);
            paint.setStrokeWidth(STROKE_WIDTH);
        

        public void save(View v) 
        
            Log.v("log_tag", "Width: " + v.getWidth());
            Log.v("log_tag", "Height: " + v.getHeight());
            if(mBitmap == null)
            
                mBitmap =  Bitmap.createBitmap (mContent.getWidth(), mContent.getHeight(), Bitmap.Config.RGB_565);;
            
            Canvas canvas = new Canvas(mBitmap);
            v.draw(canvas);

            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            mBitmap.compress(Bitmap.CompressFormat.PNG, 70, bao); 
            byte [] ba = bao.toByteArray();

            ba1=Base64.encodeToString(ba,Base64.DEFAULT);
            //Toast.makeText(SignatureCapture.this, ba1, Toast.LENGTH_LONG).show();
        

        public void clear() 
        
            path.reset();
            invalidate();
        

        @Override
        protected void onDraw(Canvas canvas) 
        
            canvas.drawPath(path, paint);
        

        @Override
        public boolean onTouchEvent(MotionEvent event) 
        
            float eventX = event.getX();
            float eventY = event.getY();
            mGetSign.setEnabled(true);

            switch (event.getAction()) 
            
            case MotionEvent.ACTION_DOWN:
                path.moveTo(eventX, eventY);
                lastTouchX = eventX;
                lastTouchY = eventY;
                return true;

            case MotionEvent.ACTION_MOVE:

            case MotionEvent.ACTION_UP:

                resetDirtyRect(eventX, eventY);
                int historySize = event.getHistorySize();
                for (int i = 0; i < historySize; i++) 
                
                    float historicalX = event.getHistoricalX(i);
                    float historicalY = event.getHistoricalY(i);
                    expandDirtyRect(historicalX, historicalY);
                    path.lineTo(historicalX, historicalY);
                
                path.lineTo(eventX, eventY);
                break;

            default:
                debug("Ignored touch event: " + event.toString());
                return false;
            

            invalidate((int) (dirtyRect.left - HALF_STROKE_WIDTH),
                    (int) (dirtyRect.top - HALF_STROKE_WIDTH),
                    (int) (dirtyRect.right + HALF_STROKE_WIDTH),
                    (int) (dirtyRect.bottom + HALF_STROKE_WIDTH));

            lastTouchX = eventX;
            lastTouchY = eventY;

            return true;
        

        private void debug(String string)
        

        private void expandDirtyRect(float historicalX, float historicalY) 
        
            if (historicalX < dirtyRect.left) 
            
                dirtyRect.left = historicalX;
             
            else if (historicalX > dirtyRect.right) 
            
                dirtyRect.right = historicalX;
            

            if (historicalY < dirtyRect.top) 
            
                dirtyRect.top = historicalY;
             
            else if (historicalY > dirtyRect.bottom) 
            
                dirtyRect.bottom = historicalY;
            
        

        private void resetDirtyRect(float eventX, float eventY) 
        
            dirtyRect.left = Math.min(lastTouchX, eventX);
            dirtyRect.right = Math.max(lastTouchX, eventX);
            dirtyRect.top = Math.min(lastTouchY, eventY);
            dirtyRect.bottom = Math.max(lastTouchY, eventY);
        
    

现在你只需要定义新类的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linearLayout1"
android:layout_
android:layout_
android:orientation="vertical" >

<LinearLayout android:layout_
    android:id="@+id/linearLayout2" android:layout_>
    <Button android:layout_ android:layout_weight=".35"
        android:text="Clear" android:layout_ android:id="@+id/clear" />
    <Button android:layout_ android:layout_weight=".35"
        android:text="Save" android:layout_ android:id="@+id/getsign" />
</LinearLayout>
<TableLayout android:layout_
    android:id="@+id/tableLayout1" android:layout_>
    <TableRow android:id="@+id/tableRow1" android:layout_
        android:layout_>
    </TableRow>
    <TableRow android:id="@+id/tableRow3" android:layout_
        android:layout_>
    </TableRow>
</TableLayout>
<LinearLayout android:layout_
    android:id="@+id/linearLayout" android:layout_ />
</LinearLayout>

现在或多或少就是这样。请注意,我不是编写活动代码的人,这些优点都不是我的,我只是简单地找到了一种如何以一种很好的方式将它组合在一起的方法。

我希望你们中的一些人会发现它有用。

【讨论】:

以上是关于适用于所有设备的 Phonegap 通用签名捕获的主要内容,如果未能解决你的问题,请参考以下文章

并行 PhoneGap 开发 - 适用于 iPhone 和 Android 的应用程序

如何在 Web 应用程序中集成签名板

JayData / Phonegap 适用于 iPad 模拟器但不适用于 iPad 设备

在 PhoneGap 中读取图像捕获文件

phonegap 签名抽奖

使用Video Control在黑莓应用程序中捕获图片仅适用于模拟器而非设备