četrtek, 29. november 2012

Oceanographic buoy Vida on mobile devices

Due to the broad interest of many users of Java ME applications for displaying the data of Oceanographic buoy Vida (OceanBuoyPiran) and tides (TideKP) and also in text format (WAP), which were released in 2007, I have recently prepared an application for mobile devices running Android.
 Application MBP can be obtained on the Android Market by searching the keywords "mbp" or "mbp vida" or directly on this link: https://play.google.com/store/apps/details?id=org.mbp&feature=search_result#?t=W251bGwsMSwyLDEsIm9yZy5tYnAiXQ
Special thanks also go to Mr. Mavricij Bizjak and the colleagues from Marine Biology Station Piran of the National Institute of Biology for the comments that have been welcomed in the development of the application.

četrtek, 8. november 2012

Android Custom UI: Horizontal Number Spinner

Hello everyone!
I would like to show how to design a custom UI element for Android the "Horizontal Number Spinner".

The final result looks like this:
Background images I have used:

Gray background
Glass background (9-patch)
Arrow

 1.) Create a custom component which extends View and implement GestureDetector.OnGestureListener interface

package si.in2.ui;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Scroller;

public class HorizontalNumberSpinner extends View implements
  GestureDetector.OnGestureListener {
 
 public static final int ARROW_BORDER = 10;
 public static final int BOTTOM_BORDER = 10;
 public static final int MARKER_DIFFERENCE = 8;
 public static final int TEXT_BORDER = 10;
 public static final int TEXT_HEIGHT = 10;
 public static final int TOP_BORDER = 10;
 private Bitmap arrowBitmap;
 
 private Paint arrowPaint;
 private Paint fontPaint;
 private Paint markerPaint;
 private Paint backgroundPaint;
 private Paint smallMarkerPaint;
 
 private GestureDetector gestureDetector;
 private OnChangeListener onChangeListener;
 private Scroller scroller;
 
 private Drawable backgroundBitmap;
 private int arrowOffset;
 private int bottomBorder;
 private int currentX;
 private int firstMarkerIndex;
 private String labelFormat;
 
 private int lastMarkerIndex;
 private int markerDifference;
 
 private int markerSpacing;
 private int maxScrollOffset;
 private double maxValue;
 private int minScrollOffset;
 private double minValue;
 
 private int scaleDivisions;
 private double scaleStep;
 private int scrollOffset;
 
 
 private double step;
 private int stepInPixels;
 private int textBorder;
 private int textHeight;
 private int topBorder;
 
 //interface for event handling
 public  interface OnChangeListener {
  public  void onValueChanged(String pString, double pDouble);
 }
 
 public HorizontalNumberSpinner(Context ctx) {
  super(ctx);
  init(ctx);
  
 }

 public HorizontalNumberSpinner(Context ctx,
   AttributeSet pAttributeSet) {
  super(ctx, pAttributeSet);
  init(ctx);
  
 }

 public HorizontalNumberSpinner(Context ctx,
   AttributeSet pAttrSet, int p) {
  super(ctx, pAttrSet, p);
  init(ctx);
  
 }

 
 private void getArrowOffset() {
  this.arrowOffset = (getMeasuredWidth() - this.arrowBitmap.getWidth() - (int) TypedValue
    .applyDimension(1, 10.0F, getResources().getDisplayMetrics()));
 }

 private int getMaxScrollOffset() {
  return (int) Math.ceil((this.maxValue - this.minValue)
    * this.scaleDivisions * this.markerSpacing / this.scaleStep);
 }

 private int getMinScrollOffset() {
  return (int) Math.ceil(this.minValue * this.scaleDivisions
    * this.markerSpacing / this.scaleStep);
 }

 /**
  * init 
  * @param ctx
  */
 private void init(Context ctx) {
  //
  scroller = new Scroller(ctx); //This class encapsulates scrolling
  gestureDetector = new GestureDetector(this);  //Detects various gestures and events using the supplied MotionEvents
  
  topBorder =  (int)TypedValue.applyDimension(1, 10, getResources().getDisplayMetrics());
  bottomBorder = (int) TypedValue.applyDimension(1, 10, getResources().getDisplayMetrics());
  markerDifference = (int) TypedValue.applyDimension(1, 8,getResources().getDisplayMetrics());
  textHeight = (int) TypedValue.applyDimension(1, 10, getResources().getDisplayMetrics());
  textBorder = (int) TypedValue.applyDimension(1, 10, getResources().getDisplayMetrics());
  
  //default values
  minValue = -200000;
  maxValue = 1000000;
  scaleStep = 1000;
  step = 100;
  scaleDivisions = 5; 
  arrowOffset = 200;
  markerSpacing = 16; 
  labelFormat = "%10.3f";
  stepInPixels = (int) ((step *  scaleDivisions *  markerSpacing) / scaleStep);
  maxScrollOffset = getMaxScrollOffset();
  minScrollOffset = getMinScrollOffset();
  
  //Log.i("SPINER", "maxScrollOffset: " + maxScrollOffset);
  //Log.i("SPINER", "minScrollOffset: " + minScrollOffset);
  
  //Here we set the background image
  this.backgroundBitmap = getResources().getDrawable(R.drawable.sp_bg_3);
  
  //copy the arrow bitmap into variable arrowBitmap 
  this.arrowBitmap = BitmapFactory.decodeResource(
    getContext().getResources(), R.drawable.spinner_pointer).copy(
    Bitmap.Config.ARGB_8888, true);
  
  
  //get the arrow offset.
  getArrowOffset();
  
  //Setup paint objects for 
  initPaintObjects();

 }

 private void initPaintObjects() 
 {
  markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  markerPaint.setColor(Color.WHITE);
  markerPaint.setStrokeWidth(3);
  markerPaint.setStyle(Paint.Style.FILL);
  
  smallMarkerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  smallMarkerPaint.setColor(Color.WHITE);
  smallMarkerPaint.setStrokeWidth(1.0F);
  smallMarkerPaint.setStyle(Paint.Style.FILL);
  smallMarkerPaint.setTextAlign(Paint.Align.CENTER);
  smallMarkerPaint.setTextSize(10);
  
  fontPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  fontPaint.setStyle(Paint.Style.STROKE);
  fontPaint.setColor(Color.WHITE);
  fontPaint.setStrokeWidth(1);
  fontPaint.setAntiAlias(true);
  fontPaint.setTextSize(12);
  fontPaint.setTextAlign(Paint.Align.CENTER);  
  
  backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

  arrowPaint = new Paint();
  
  }

 
 private int measureHeigth(int height) {
  View.MeasureSpec.getMode(height);
  return View.MeasureSpec.getSize(height);
 }

 private int measureWidth(int width) {
  View.MeasureSpec.getMode(width);
  return View.MeasureSpec.getSize(width);
 }

 private void notifyListener() {

  if (this.onChangeListener != null) {
   double d = getValue();
      //fire event 
   onChangeListener.onValueChanged(String.format(this.getFormat(), d), d);
  }
 }

 private void scrollToSelectableValue() { 
  
  int val = Math.round(this.scrollOffset / this.stepInPixels) * this.stepInPixels;
  scroller.startScroll(scrollOffset, 0, val - this.scrollOffset, 0);
  Log.i("SPINER", "scrollToSelectableValue val="+ val +"," + scrollOffset);
  post(new Runnable() {
   public void run() {
    if (!scroller.isFinished()) {
     postDelayed(this, 20);
     invalidate(); //correct the value on the spinner     
     return; 
    }
   }
  });
 }

 

 @Override
 public boolean onDown(MotionEvent paramMotionEvent) {
  Log.i("SPINER", "onDown: ");
  
  scroller.abortAnimation();
  scrollToSelectableValue();
  
  return true;
 }

 @Override
 public void onDraw(Canvas pCanvas) {
  
  //check if the scroller is not finished
  if (scroller.isFinished() == false) 
  {
   scroller.computeScrollOffset();
   scrollOffset = scroller.getCurrX(); //get the scroller currnet x value
   notifyListener(); 
  }

  this.backgroundBitmap.setBounds(0, 0, getMeasuredWidth(),getMeasuredHeight());
  this.backgroundBitmap.draw(pCanvas);
  
  //calcukate curentx, first and last marker index...
  this.firstMarkerIndex = ((-this.scrollOffset - this.arrowOffset) / this.markerSpacing);
  this.firstMarkerIndex -= (this.scaleDivisions - this.firstMarkerIndex) % this.scaleDivisions;
  this.currentX = (this.arrowOffset + this.markerSpacing * this.firstMarkerIndex + this.scrollOffset);
  this.lastMarkerIndex = ((-this.scrollOffset - this.arrowOffset + getMeasuredWidth()) / this.markerSpacing);
  this.lastMarkerIndex += this.scaleDivisions - this.lastMarkerIndex % this.scaleDivisions;

  int chk = firstMarkerIndex;
  while(true) {
   if (chk > lastMarkerIndex) {
    pCanvas.drawBitmap(arrowBitmap,
      arrowOffset - (arrowBitmap.getWidth() / 2), 0.0F,
      backgroundPaint);
    return;
   }

   if (chk % scaleDivisions == 0) {
    double d =  (chk / scaleDivisions) * scaleStep;
    
    //draw the leading line
    pCanvas.drawLine(currentX, topBorder, currentX,
      getMeasuredHeight() - bottomBorder - textHeight
        - textBorder, markerPaint);
   
     //draw text below the leading line
    pCanvas.drawText(String.format(labelFormat, d), currentX, getMeasuredHeight() - bottomBorder, fontPaint);
    
     currentX +=  markerSpacing;
    
   } else {
    //lines between leading lines
    pCanvas.drawLine(currentX, topBorder, currentX,
      getMeasuredHeight() - bottomBorder - textHeight
        - textBorder - markerDifference,
      smallMarkerPaint);
    currentX +=  markerSpacing;
   }
   
   chk++;
  } 

 }

 @Override
 public boolean onFling(MotionEvent pMotionEvent1,
   MotionEvent pMotionEvent2, float x, float y) {
  Log.i("SPINER", "onFling: " + x + ", " + y );
  scroller.fling(this.scrollOffset, 0, (int) x,
    (int) y, -this.maxScrollOffset,
    -this.minScrollOffset, 0, 0);
  //start a thread and after 20msec verify if scroller is finished then repaint object..
  post(new Runnable() {
   public void run() {
    if (!scroller.isFinished()) {
     invalidate();
     postDelayed(this, 20);
    } else {
     scrollToSelectableValue();

    }
   }
  });
  return false;
 }

 
 protected void onMeasure(int w, int h) {
  Log.i("SPINER", "onMeasure: " + w + ", " + h );
  int i = measureHeigth(w);
  int j = measureWidth(h);
  getArrowOffset();
  setMeasuredDimension(j, i);
 }

 public boolean onScroll(MotionEvent paramMotionEvent1,
   MotionEvent paramMotionEvent2, float x, float y) {
  Log.i("SPINER", "onScroll: " + x + ", " + y );
  this.scrollOffset = (int) (this.scrollOffset - x);

  if (this.scrollOffset < -this.maxScrollOffset) {
   this.scrollOffset = -this.maxScrollOffset;
  }
  invalidate();
  notifyListener();
  return true;

 }

 @Override
 public void onShowPress(MotionEvent pMotionEvent) {
  return;
 }

 @Override
 public boolean onSingleTapUp(MotionEvent pMotionEvent) {
  return false;
 }

 @Override
 public boolean onTouchEvent(MotionEvent pMotionEvent) {
  if ((pMotionEvent.getAction() == MotionEvent.ACTION_UP) && (this.scroller.isFinished()))
   scrollToSelectableValue();
  this.gestureDetector.onTouchEvent(pMotionEvent);
  return true;
 }

 public void setBackgroundBitmap(Drawable pDrawable) {
  this.backgroundBitmap = pDrawable;
 }

 public void setFormat(String format) {
  this.labelFormat = format;
 }

 public void setMaxValue(double mValue) {
  this.maxValue = mValue;
  this.maxScrollOffset = getMaxScrollOffset();
 }

 public void setMinValue(double minValue) {
  this.minValue = minValue;
  this.minScrollOffset = getMinScrollOffset();
  this.maxScrollOffset = getMaxScrollOffset();
 }

 public void setOnChangeListener(OnChangeListener pOnChangeListener) {
  this.onChangeListener = pOnChangeListener;
 }

 public void setScaleDivisions(int i) {
  scaleDivisions = i;
  minScrollOffset = getMinScrollOffset();
  maxScrollOffset = getMaxScrollOffset();
 }

 public void setScaleStep(double d) {
  scaleStep = d;
  step = d /  (2 * scaleDivisions);
  stepInPixels = (int) ((step * scaleDivisions *  markerSpacing) / d);
  minScrollOffset = getMinScrollOffset();
  maxScrollOffset = getMaxScrollOffset();
 }

 public void setValue(double d) {
  scrollOffset = (int) ((-d *  (scaleDivisions * markerSpacing)) / scaleStep);
  invalidate();
  scrollToSelectableValue();
 }
 
 public String getFormat() {
  return this.labelFormat;
 }

 public double getMaxValue() {
  return this.maxValue;
 }

 public double getMinValue() {
  return this.minValue;
 }

 public int getScaleDivisions() {
  return this.scaleDivisions;
 }

 public double getScaleStep() {
  return this.scaleStep;
 }

 public double getStep() {
  return this.step;
 }

 public double getValue() {
  return ( (-scrollOffset) * scaleStep)
    /  (scaleDivisions * markerSpacing);
 }

 @Override
 public void onLongPress(MotionEvent arg0) {
  // TODO Auto-generated method stub
 return;
  
 }
}

2.Android Layout file




    
    
    

    
    



3. HorizontalSpinnerActivity file
In the "main" activity we initialize our HorizontalSpinner with some values:

package si.in2.ui;

import android.app.Activity;
import android.os.Bundle;

import android.widget.LinearLayout;
import android.widget.TextView;

public class HorizontalSpinnerActivity extends Activity  {
    /** Called when the activity is first created. */
 
 HorizontalNumberSpinner spinner;
 LinearLayout layout;
 TextView text;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        text = (TextView)  findViewById(R.id.textView1);
       
        spinner = (HorizontalNumberSpinner)findViewById(R.id.spiner);
        spinner.setMaxValue(10000.0); // set the maximum value
        spinner.setMinValue(-10000.0);// set the minimum value
        spinner.setScaleStep(50.0); // step for 50 values
        spinner.setScaleDivisions(5); // set scale division between numbers
        spinner.setFormat("%.000f"); // set the format displayed below leading line
        spinner.setValue(5000.0); // start at value
        
        //event handling
        spinner.setOnChangeListener(new HorizontalNumberSpinner.OnChangeListener()
        {
          public void onValueChanged(String paramString, double paramDouble)
          {
            text.setText("Value: " + paramDouble);
          }
        });
        
      
    }

 
}