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