I would like to show how to design a custom UI element for Android the "Horizontal Number Spinner".
The final result looks like this:
![]() |
| 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);
}
});
}
}





There's a problem with the zip file! Can you upload it again?
OdgovoriIzbriši