Question

I will like accomplish the following thing:

Get a signature using a touchscreen device, a signature which looks good, and there the screen is responsive when the user signs, and it works cross-platform.

Emil

Was it helpful?

Solution

After 2 weeks of digging and coding and testing and failing and reading and retrying I have manage to put something together:

Possible solutions:

  1. Native app code for each platform, but than you have multiple applications not one
  2. Use canvas, almost good, just on Android there are 2 issues:
    • Android browser tends to skip quite a few touch events, so when drawing signature in canvas looks very poor
    • On devices running 2.3 and below you can face difficulty with converting canvas to base64 since function is not supported

My solution to this was: - Use PhoneGap, and use native signature capture activity on Android and canvas on all other devices. Easier to be said than to be done as it turned out. After a lot of struggle I have managed to put it together, and this is what I will like to share with you, this is just the backbone so you can implement it as you want in your own project.

First of all, you will need to have phonegap installed, than you should create a new project. You will need to have the following 2 plugins: InAppBrowser and Device, please see documentation how to get this.

Once the project is created, use the following bit of code for your index.html (note jq.js is a local copy of jquery so the app can run offline):

<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>

Now we need a file called sign.html which will grab our signature if we are not on Android, here is the code (note jq.js is the same local copy of 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' width="+width+" height="+height+" 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>

All good, almost ready to get going, before you do that make sure you save a copy of current jQuery to your www folder under the name of jq.js Build the app with phonegap for the desired platform (iOS, Android, Windows Phone, BB).

After you build it, go the Android build and modify the main activity java to the following:

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
         }
      }
    }
}

Create a new class called SignatureCapture, make sure you define it in your mainfest file as well:

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);
        }
    }
}

Now you only need to define the layout for the new class:

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

<LinearLayout android:layout_height="wrap_content"
    android:id="@+id/linearLayout2" android:layout_width="match_parent">
    <Button android:layout_height="50dp" android:layout_weight=".35"
        android:text="Clear" android:layout_width="0dp" android:id="@+id/clear" />
    <Button android:layout_height="50dp" android:layout_weight=".35"
        android:text="Save" android:layout_width="0dp" android:id="@+id/getsign" />
</LinearLayout>
<TableLayout android:layout_height="wrap_content"
    android:id="@+id/tableLayout1" android:layout_width="match_parent">
    <TableRow android:id="@+id/tableRow1" android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </TableRow>
    <TableRow android:id="@+id/tableRow3" android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </TableRow>
</TableLayout>
<LinearLayout android:layout_height="match_parent"
    android:id="@+id/linearLayout" android:layout_width="match_parent" />
</LinearLayout>

Now more or less this is it. Please note I'm NOT the person who wrote the activity code, and none of that merit is mine, i just simply found a way how to put it together in a nice and good way.

I hope that some of you will find it useful.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top