diff options
author | fred ross-perry <fredross-perry@Fred-Ross-Perrys-Computer.local> | 2016-08-26 09:50:48 -0700 |
---|---|---|
committer | fred ross-perry <fredross-perry@Fred-Ross-Perrys-Computer.local> | 2016-09-14 08:53:32 -0700 |
commit | ffbe3db71ea0f96b408e22418547a8ff898f380e (patch) | |
tree | 2f34407ef3c229a949b0a2b514e307297437a169 /platform | |
parent | e18d11b63af0ca8a302f23b32ffc24578c830989 (diff) | |
download | mupdf-ffbe3db71ea0f96b408e22418547a8ff898f380e.tar.xz |
Android example - drawing ink annotations
This commit puts in the UI for drawing with color
and line thickness. But it does not yet save this to
the document.
Diffstat (limited to 'platform')
42 files changed, 3875 insertions, 35 deletions
diff --git a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/ColorDialog.java b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/ColorDialog.java new file mode 100644 index 00000000..5da38c2e --- /dev/null +++ b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/ColorDialog.java @@ -0,0 +1,204 @@ +package com.artifex.mupdf.android; + + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Point; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.artifex.mupdf.fitz.R; + +public class ColorDialog implements View.OnTouchListener, PopupWindow.OnDismissListener { + + // these are set in the constructor. + private final Context mContext; + private final View mAnchor; + private final ColorChangedListener mListener; + private final int mDialogType; + private boolean mAutoDismiss = false; + + // only one ColorDialog at a time please. + private static ColorDialog singleton = null; + + // a popup window for us. + private PopupWindow popupWindow; + + // two different variants of this dilog + public static final int FG_COLORS = 1; + public static final int BG_COLORS = 2; + + // allowable foreground colors. These come from UE2FileViewerApp + private final String mFgColors[] = {"#000000", "#FFFFFF", "#D8D8D8", "#808080", "#EEECE1", "#1F497D", + "#0070C0", "#C0504D", "#9BBB59", "#8064A2", "#4BACC6", "#F79646", "#FF0000", + "#FFFF00", "#DBE5F1", "#F2DCDB", "#EBF1DD", "#00B050"}; + + // constructor + public ColorDialog(int dialogType, Context context, View anchor, ColorChangedListener listener, boolean autoDismiss) + { + mContext = context; + mAnchor = anchor; + mListener = listener; + mDialogType = dialogType; + mAutoDismiss = autoDismiss; + } + + // whether or not to show the title + private boolean mShowTitle = true; + public void setShowTitle(boolean val) {mShowTitle = val;} + + // time to show the dialog + public void show() + { + // remember us + singleton = this; + + // get the layout + View popupView = LayoutInflater.from(mContext).inflate(R.layout.colors, null); + + // set the title + TextView tv = (TextView)popupView.findViewById(R.id.color_dialog_title); + if (mShowTitle) { + if (mDialogType == BG_COLORS) + tv.setText(mContext.getString(R.string.background)); + else + tv.setText(mContext.getString(R.string.color)); + } + else { + tv.setVisibility(View.GONE); + } + + // choose the list of colors to use + String colors[] = mFgColors; + + // get the three rows of buttons from the layout + LinearLayout rows[] = new LinearLayout[3]; + rows[0] = (LinearLayout)popupView.findViewById(R.id.fontcolors_row1); + rows[1] = (LinearLayout)popupView.findViewById(R.id.fontcolors_row2); + rows[2] = (LinearLayout)popupView.findViewById(R.id.fontcolors_row3); + + // Set up a button for each color. + // Hide buttons for which there are no colors. + int icolor = 0; + int irow; + for (irow=0; irow<rows.length; irow++) { + LinearLayout row = rows[irow]; + int count = row.getChildCount(); + for (int i=0; i<count; i++) { + Button button = (Button)row.getChildAt(i); + if (icolor+1 <= colors.length) { + button.setVisibility(View.VISIBLE); + button.setBackgroundColor(Color.parseColor(colors[icolor])); + button.setTag(colors[icolor]); + button.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) + { + mListener.onColorChanged((String)v.getTag()); + if (mAutoDismiss) + dismiss(); + } + }); + } + else { + button.setVisibility(View.GONE); + } + icolor++; + } + } + + // Set up the transaparent button if we're doing background colors. + Button tpb = (Button) popupView.findViewById(R.id.transparent_color_button); + if (mDialogType==ColorDialog.BG_COLORS) { + tpb.setVisibility(View.VISIBLE); + tpb.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) + { + mListener.onColorChanged((String)v.getTag()); + } + }); + } + else + tpb.setVisibility(View.GONE); + + // get screen width + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = new DisplayMetrics(); + wm.getDefaultDisplay().getMetrics(metrics); + int screenWidth = metrics.widthPixels; + + // put everything in a popup window and show it. + // by default that's the upper right corner, but the dialog is draggable. + popupWindow = new PopupWindow(popupView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + popupWindow.setFocusable(true); + popupView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + popupWindow.showAtLocation(mAnchor, Gravity.TOP|Gravity.LEFT, screenWidth-popupView.getMeasuredWidth()-15, 100); + popupWindow.setClippingEnabled(false); + // this enables dragging + popupView.setOnTouchListener(this); + // this allows us to know when the popup is being dismissed + popupWindow.setOnDismissListener(this); + } + + // internal function to dismiss the popup. + public void dismiss() + { + popupWindow.dismiss(); + singleton = null; + } + + // static function to dismiss the popup. + static public void finish() + { + if (singleton!=null) + singleton.dismiss(); + } + + // this function is called when the user taps outside the popup. + // we make sure to dismiss it properly. + @Override + public void onDismiss() { + dismiss(); + } + + // we're an onTouch listener for the popup window. + // we use that to allow for dragging the popup around. + + private int start[]; + private final Point down = new Point(); + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + + start = new int[2]; + popupWindow.getContentView().getLocationOnScreen(start); + down.set((int) event.getRawX(), (int) event.getRawY()); + break; + + case MotionEvent.ACTION_MOVE: + + int dx = down.x - (int)event.getRawX(); + int dy = down.y - (int)event.getRawY(); + popupWindow.update(start[0]-dx, start[1]-dy,-1, -1, true); + break; + } + return true; + } + + public interface ColorChangedListener { + void onColorChanged(String color); + } + +} diff --git a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocActivityView.java b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocActivityView.java index e56ed43d..26624ab5 100644 --- a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocActivityView.java +++ b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocActivityView.java @@ -2,6 +2,7 @@ package com.artifex.mupdf.android; import android.app.Activity; import android.content.Context; +import android.graphics.Color; import android.graphics.PorterDuff; import android.util.AttributeSet; import android.util.Log; @@ -26,7 +27,7 @@ import com.artifex.mupdf.fitz.Link; import com.artifex.mupdf.fitz.Outline; import com.artifex.mupdf.fitz.R; -public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeListener, View.OnClickListener +public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeListener, View.OnClickListener, DocView.SelectionChangeListener { private DocView mDocView; private DocReflowView mDocReflowView; @@ -55,7 +56,15 @@ public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeL private ImageButton mPrintButton; private ImageButton mShareButton; private ImageButton mOpenInButton; + private ImageButton mToggleAnnotButton; + private ImageButton mHighlightButton; + private ImageButton mDeleteButton; + + private ImageButton mNoteButton; + private ImageButton mDrawButton; + private ImageButton mLineColorButton; + private ImageButton mLineThicknessButton; public DocActivityView(Context context) { @@ -356,6 +365,23 @@ public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeL mToggleAnnotButton = (ImageButton)findViewById(R.id.show_annot_button); mToggleAnnotButton.setOnClickListener(this); + mHighlightButton = (ImageButton)findViewById(R.id.highlight_button); + mHighlightButton.setOnClickListener(this); + + mNoteButton = (ImageButton)findViewById(R.id.note_button); + mNoteButton.setOnClickListener(this); + + mDrawButton = (ImageButton)findViewById(R.id.draw_button); + mDrawButton.setOnClickListener(this); + + mLineColorButton = (ImageButton)findViewById(R.id.line_color_button); + mLineColorButton.setOnClickListener(this); + + mLineThicknessButton = (ImageButton)findViewById(R.id.line_thickness_button); + mLineThicknessButton.setOnClickListener(this); + + mDeleteButton = (ImageButton)findViewById(R.id.delete_button); + mDeleteButton.setOnClickListener(this); mDoc = new Document(path); @@ -409,6 +435,10 @@ public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeL { mDocPagesView.clone(mDocView); } + + mHighlightButton.setEnabled(false); + mDocView.setSelectionChangeListener(this); + onSelectionChanged(); } public void showUI(boolean show) @@ -481,10 +511,12 @@ public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeL { if (v == mReflowButton) onReflowButton(); + if (v == mFirstPageButton) onFirstPageButton(); if (v == mLastPageButton) onLastPageButton(); + if (v == mSearchButton) onShowSearch(); if (v == mSearchText) @@ -493,6 +525,7 @@ public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeL onSearchNextButton(); if (v == mSearchPreviousButton) onSearchPreviousButton(); + if (v == mBackButton) onBackButton(); @@ -506,8 +539,22 @@ public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeL onShareButton(); if (v == mOpenInButton) onOpenInButton(); + if (v == mToggleAnnotButton) onToggleAnnotButton(); + if (v == mHighlightButton) + onHighlightButton(); + if (v == mDeleteButton) + onDeleteButton(); + + if (v == mNoteButton) + onNoteButton(); + if (v == mDrawButton) + onDrawButton(); + if (v == mLineColorButton) + onLineColorButton(); + if (v == mLineThicknessButton) + onLineThicknessButton(); } public void onSearchNextButton() @@ -647,6 +694,83 @@ public class DocActivityView extends FrameLayout implements TabHost.OnTabChangeL mDocView.toggleAnnotations(); } + private void onHighlightButton() + { + mDocView.onHighlight(); + } + + private void onNoteButton() + { + mDocView.onNoteMode(); + } + + private void onDrawButton() + { + mDocView.onDrawMode(); + } + + private void onLineColorButton() + { + if (mDocView.getDrawMode() || mDocView.hasInkAnnotationSelected()) + { + ColorDialog dlg = new ColorDialog(ColorDialog.BG_COLORS, + getContext(), mLineColorButton, new ColorDialog.ColorChangedListener() + { + @Override + public void onColorChanged(String color) + { + int icolor = Color.parseColor(color); + mDocView.setInkLineColor(icolor); + mLineColorButton.setColorFilter(icolor, PorterDuff.Mode.SRC_IN); + } + }, true); + dlg.setShowTitle(false); + dlg.show(); + } + } + + private void onLineThicknessButton() + { + if (mDocView.getDrawMode() || mDocView.hasInkAnnotationSelected()) + { + float val = mDocView.getInkLineThickness(); + LineWidthDialog.show(getContext(), mLineThicknessButton, val, + new LineWidthDialog.WidthChangedListener() + { + @Override + public void onWidthChanged(float value) + { + mDocView.setInkLineThickness(value); + } + }); + } + } + + private void onDeleteButton() + { + mDocView.onDelete(); + } + + public void onSelectionChanged() + { + boolean hasSel = mDocView.hasSelection(); + boolean hasInkAnnotSel = mDocView.hasInkAnnotationSelected(); + + mHighlightButton.setEnabled(hasSel); + + boolean noteMode = mDocView.getNoteMode(); + mNoteButton.setSelected(noteMode); + findViewById(R.id.note_holder).setSelected(noteMode); + + boolean drawMode = mDocView.getDrawMode(); + mDrawButton.setSelected(drawMode); + mLineColorButton.setEnabled(drawMode || hasInkAnnotSel); + mLineThicknessButton.setEnabled(drawMode || hasInkAnnotSel); + mDeleteButton.setEnabled(!drawMode && hasInkAnnotSel); + + findViewById(R.id.draw_tools_holder).setSelected(drawMode); + } + private OnDoneListener mDoneListener = null; public void setOnDoneListener(OnDoneListener l) {mDoneListener = l;} public interface OnDoneListener diff --git a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocPageView.java b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocPageView.java index 50f526f6..0bde1f03 100755 --- a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocPageView.java +++ b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocPageView.java @@ -7,10 +7,13 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.os.AsyncTask; import android.support.v4.content.ContextCompat; import android.util.Log; +import android.util.TypedValue; import android.view.KeyEvent.Callback; import android.view.View; import android.view.ViewGroup; @@ -27,6 +30,8 @@ import com.artifex.mupdf.fitz.StructuredText; import com.artifex.mupdf.fitz.android.AndroidDrawDevice; import java.util.ArrayList; +import java.util.Iterator; +import java.util.ListIterator; public class DocPageView extends View implements Callback { @@ -550,6 +555,17 @@ public class DocPageView extends View implements Callback canvas.drawRect(mHighlightingRect, mSearchHighlightPainter); } + // draw ink annotations + if (mInkAnnots != null) + { + Iterator<InkAnnotation> it = mInkAnnots.iterator(); + while (it.hasNext()) + { + InkAnnotation annot = it.next(); + annot.draw(canvas); + } + } + // draw blue dot if (isMostVisible) { @@ -668,11 +684,24 @@ public class DocPageView extends View implements Callback return screenToPage(p.x, p.y); } + public PointF screenToPage(PointF p) + { + Point pp = screenToPage(new Point((int)p.x, (int)p.y)); + return new PointF(pp.x, pp.y); + } + private double getFactor() { return mZoom * mScale * mResolution / 72f; } + private double pageToScreen(double val) + { + double factor = getFactor(); + + return factor * val; + } + private Point screenToPage(int screenX, int screenY) { // convert to view-relative @@ -702,6 +731,36 @@ public class DocPageView extends View implements Callback return new Point(viewX, viewY); } + public void pageToView(Point pageP, Point viewP) + { + double factor = getFactor(); + + int x = (int) (((double) pageP.x) * factor); + int y = (int) (((double) pageP.y) * factor); + + viewP.set(x, y); + } + + public void pageToView(PointF pageP, PointF viewP) + { + double factor = getFactor(); + + float x = (pageP.x * (float)factor); + float y = (pageP.y * (float)factor); + + viewP.set(x, y); + } + + private PointF pageToView(PointF pageP) + { + double factor = getFactor(); + + float x = (pageP.x * (float)factor); + float y = (pageP.y * (float)factor); + + return new PointF(x, y); + } + public void pageToView(Rect pageR, Rect viewR) { double factor = getFactor(); @@ -751,6 +810,35 @@ public class DocPageView extends View implements Callback public boolean onSingleTap(int x, int y) { + // see if an ink annotation has been tapped on + if (mInkAnnots != null) + { + boolean hit = false; + + // switch to page coordinates + Point pt = screenToPage(x, y); + + // iterate in reverse order + ListIterator<InkAnnotation> li = mInkAnnots.listIterator(mInkAnnots.size()); + while (li.hasPrevious()) + { + InkAnnotation annot = li.previous(); + annot.setSelected(false); + if(annot.hitTest(pt)) + { + if (!hit) + annot.setSelected(true); + hit = true; + } + } + + if (hit) + { + invalidate(); + return true; + } + } + // NOTE: when double-tapping, a single-tap will also happen first. // so that must be safe to do. @@ -765,17 +853,53 @@ public class DocPageView extends View implements Callback // during layout, a DocView-relative rect is calculated and stashed here. private final Rect mChildRect = new Rect(); - public void setChildRect(Rect r) { mChildRect.set(r); } - public Rect getChildRect() { return mChildRect; } + private ArrayList<InkAnnotation> mInkAnnots; + + public void startDraw(float x, float y, int color, float thickness) + { + // create annotation list + if (mInkAnnots == null) + mInkAnnots = new ArrayList<>(); + + // add a new annotation to the list + // convert thickness from pt to pixels + float px = thickness * (float)getFactor(); + InkAnnotation annot = new InkAnnotation(color, px); + mInkAnnots.add(annot); + + // add first point to the new annot, in page coords + PointF pScreen = new PointF(x, y); + PointF pPage = screenToPage(pScreen); + annot.add(pPage); + + invalidate(); + } + + public void continueDraw(float x, float y) + { + if (mInkAnnots!=null && mInkAnnots.size()>0) + { + // get the most recent annotation + InkAnnotation annot = mInkAnnots.get(mInkAnnots.size()-1); + + // add the point, in page coords + PointF pScreen = new PointF(x, y); + PointF pPage = screenToPage(pScreen); + annot.add(pPage); + + invalidate(); + } + } + private class RenderTaskParams { RenderTaskParams(RenderListener listener, Matrix ctm, Bitmap bitmap, @@ -865,4 +989,247 @@ public class DocPageView extends View implements Callback } } + private InkAnnotation getSelectedInkAnnotation() + { + if (mInkAnnots != null) + { + // iterate in reverse order + ListIterator<InkAnnotation> li = mInkAnnots.listIterator(mInkAnnots.size()); + while (li.hasPrevious()) + { + InkAnnotation annot = li.previous(); + if (annot.isSelected()) + return annot; + } + } + + return null; + } + + public boolean hasInkAnnotationSelected() + { + return (getSelectedInkAnnotation() != null); + } + + public void setSelectedInkLineColor(int val) + { + InkAnnotation annot = getSelectedInkAnnotation(); + if (annot != null) + { + annot.setLineColor(val); + invalidate(); + } + } + + public void setSelectedInkLineThickness(float val) + { + InkAnnotation annot = getSelectedInkAnnotation(); + if (annot != null) + { + float px = val * (float)getFactor(); + annot.setLineThickness(px); + invalidate(); + } + } + + public void deleteSelectedInkAnnotation() + { + InkAnnotation annot = getSelectedInkAnnotation(); + if (annot != null) + { + mInkAnnots.remove(annot); + invalidate(); + } + } + +//----------------------------------------------------- + + public class InkAnnotation + { + private float mLineThickness; + public void setLineThickness(float lineThickness) {mLineThickness = lineThickness;} + + private int mLineColor; + public void setLineColor(int lineColor) {mLineColor = lineColor;} + + private boolean mSelected = false; + public void setSelected(boolean sel) {mSelected = sel;} + public boolean isSelected() {return mSelected;} + + private ArrayList<PointF> mArc; + + // touch margin is 2mm either side of the arc + private final float HIT_MARGIN = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 2, + getResources().getDisplayMetrics()); + + public InkAnnotation(int lineColor, float lineThickness) + { + mLineColor = lineColor; + mLineThickness = lineThickness; + mArc = new ArrayList<>(); + } + + public void add(PointF p) + { + mArc.add(p); + } + + public boolean hitTest(Point pt) + { + PointF p1, p2; + PointF ptf = new PointF(pt); + + if (mArc.size() >= 2) + { + Iterator<PointF> iit = mArc.iterator(); + p1 = iit.next(); + while (iit.hasNext()) + { + p2 = iit.next(); + + // test + double d = LineToPointDistance2D(p1, p2, ptf); + d = pageToScreen(d); + + if (d <= HIT_MARGIN) + { + return true; + } + + p1 = p2; + } + } + else + { + p1 = mArc.get(0); + + // test + double d = Distance(p1, ptf); + d = pageToScreen(d); + + if (d <= HIT_MARGIN) + { + return true; + } + + } + + return false; + } + + public void draw(Canvas canvas) + { + Path path = new Path(); + PointF pPage; + PointF pView = new PointF(); + + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setDither(true); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStyle(Paint.Style.FILL); + paint.setStrokeWidth(mLineThickness * mScale); + paint.setColor(mLineColor); + + RectF bounds = new RectF(); + + if (mArc.size() >= 2) + { + Iterator<PointF> iit = mArc.iterator(); + pPage = iit.next(); + pageToView(pPage, pView); + float mX = pView.x; + float mY = pView.y; + path.moveTo(mX, mY); + while (iit.hasNext()) + { + pPage = iit.next(); + pageToView(pPage, pView); + float x = pView.x; + float y = pView.y; + path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); + mX = x; + mY = y; + } + path.lineTo(mX, mY); + paint.setStyle(Paint.Style.STROKE); + canvas.drawPath(path, paint); + + path.computeBounds(bounds, true); + } + else + { + pPage = mArc.get(0); + pageToView(pPage, pView); + float r = mLineThickness * mScale / 2; + bounds.set(pView.x-r, pView.y-r, pView.x+r, pView.y+r); + canvas.drawCircle(pView.x, pView.y, r, paint); + } + + if (isSelected()) + { + // expand the bounds to account for ine thickness + float px = mLineThickness * mScale / 2; + bounds.inset(-px, -px); + + mHighlightingRect.set((int)bounds.left, (int)bounds.top, (int)bounds.right, (int)bounds.bottom); + canvas.drawRect(mHighlightingRect, mSelectionHighlightPainter); + } + } + + private double DotProduct(PointF pointA, PointF pointB, PointF pointC) + { + double[] AB = new double[2]; + double[] BC = new double[2]; + AB[0] = pointB.x - pointA.x; + AB[1] = pointB.y - pointA.y; + BC[0] = pointC.x - pointB.x; + BC[1] = pointC.y - pointB.y; + double dot = AB[0] * BC[0] + AB[1] * BC[1]; + + return dot; + } + + //Compute the cross product AB x AC + private double CrossProduct(PointF pointA, PointF pointB, PointF pointC) + { + double[] AB = new double[2]; + double[] AC = new double[2]; + AB[0] = pointB.x - pointA.x; + AB[1] = pointB.y - pointA.y; + AC[0] = pointC.x - pointA.x; + AC[1] = pointC.y - pointA.y; + double cross = AB[0] * AC[1] - AB[1] * AC[0]; + + return cross; + } + + //Compute the distance from A to B + double Distance(PointF pointA, PointF pointB) + { + double d1 = pointA.x - pointB.x; + double d2 = pointA.y - pointB.y; + + return Math.sqrt(d1 * d1 + d2 * d2); + } + + //Compute the distance from AB to C + double LineToPointDistance2D(PointF pointA, PointF pointB, PointF pointC) + { + double dist = CrossProduct(pointA, pointB, pointC) / Distance(pointA, pointB); + if (true) + { + double dot1 = DotProduct(pointA, pointB, pointC); + if (dot1 > 0) + return Distance(pointB, pointC); + + double dot2 = DotProduct(pointB, pointA, pointC); + if (dot2 > 0) + return Distance(pointA, pointC); + } + return Math.abs(dist); + } + } + } diff --git a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocView.java b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocView.java index 346b887b..11ad2601 100644 --- a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocView.java +++ b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/DocView.java @@ -7,8 +7,9 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; -import android.view.View; +import android.view.MotionEvent; import android.widget.RelativeLayout; +import android.widget.Toast; import com.artifex.mupdf.fitz.Page; import com.artifex.mupdf.fitz.R; @@ -109,11 +110,6 @@ public class DocView extends DocViewBase implements DragHandleListener mSelectionHandleBottomRight.show(show); } - private boolean getSelectionHandlesVisible() - { - return (mSelectionHandleTopLeft.getVisibility() == View.VISIBLE); - } - @Override protected void doSingleTap(float fx, float fy) { @@ -123,18 +119,20 @@ public class DocView extends DocViewBase implements DragHandleListener if (dpv == null) return; - if (getSelectionHandlesVisible()) + if (!getDrawMode()) { - // hide handles and remove selection from pages - showSelectionHandles(false); - int numPages = getPageCount(); - for (int i = 0; i < numPages; i++) + if (dpv.onSingleTap(p.x, p.y)) { - DocPageView cv = (DocPageView) getOrCreateChild(i); - cv.removeSelection(); - if (cv.isReallyVisible()) - cv.invalidate(); + onChangeSelection(); + return; } + onChangeSelection(); + } + + if (hasSelection()) + { + clearSelection(); + onChangeSelection(); } else { @@ -142,20 +140,88 @@ public class DocView extends DocViewBase implements DragHandleListener Rect r = dpv.selectWord(p); if (r != null) { - // show handles showSelectionHandles(true); - // set highlight boundaries selectionStartPage = dpv; selectionStartLoc.set(r.left, r.top); selectionEndPage = dpv; selectionEndLoc.set(r.right, r.bottom); moveHandlesToCorners(); + + onChangeSelection(); } } } + private void clearSelection() + { + selectionStartPage = null; + selectionEndPage = null; + showSelectionHandles(false); + int numPages = getPageCount(); + for (int i = 0; i < numPages; i++) + { + DocPageView cv = (DocPageView) getOrCreateChild(i); + cv.removeSelection(); + if (cv.isReallyVisible()) + cv.invalidate(); + } + } + + public boolean hasSelection() + { + return (selectionStartPage != null && selectionEndPage != null); + } + + public boolean hasInkAnnotationSelected() + { + int numPages = getPageCount(); + for (int i = 0; i < numPages; i++) + { + DocPageView cv = (DocPageView) getOrCreateChild(i); + if (cv.hasInkAnnotationSelected()) + return true; + } + + return false; + } + + public void setSelectedInkLineColor(int val) + { + int numPages = getPageCount(); + for (int i = 0; i < numPages; i++) + { + DocPageView cv = (DocPageView) getOrCreateChild(i); + if (cv.hasInkAnnotationSelected()) + cv.setSelectedInkLineColor(val); + } + } + + public void setSelectedInkLineThickness(float val) + { + int numPages = getPageCount(); + for (int i = 0; i < numPages; i++) + { + DocPageView cv = (DocPageView) getOrCreateChild(i); + if (cv.hasInkAnnotationSelected()) + cv.setSelectedInkLineThickness(val); + } + } + + private void onChangeSelection() + { + if (mSelectionChangeListener != null) + mSelectionChangeListener.onSelectionChanged(); + } + + private SelectionChangeListener mSelectionChangeListener = null; + public void setSelectionChangeListener (SelectionChangeListener l) {mSelectionChangeListener = l;} + public interface SelectionChangeListener + { + public void onSelectionChanged(); + } + @Override protected void doDoubleTap(float fx, float fy) { @@ -214,6 +280,7 @@ public class DocView extends DocViewBase implements DragHandleListener selectionStartPage = pageView1; p1 = pageView1.screenToPage(p1); selectionStartLoc.set(p1.x, p1.y); + onChangeSelection(); } } @@ -228,6 +295,7 @@ public class DocView extends DocViewBase implements DragHandleListener selectionEndPage = pageView2; p2 = pageView2.screenToPage(p2); selectionEndLoc.set(p2.x, p2.y); + onChangeSelection(); } } @@ -417,4 +485,129 @@ public class DocView extends DocViewBase implements DragHandleListener } } + public void onHighlight() + { + if (hasSelection()) + { + Toast.makeText(getContext(),"onHighlight", Toast.LENGTH_SHORT).show(); + } + } + + private boolean mNoteMode = false; + public boolean getNoteMode() {return mNoteMode;} + public void onNoteMode() + { + mNoteMode = !mNoteMode; + mDrawMode = false; + clearSelection(); + onChangeSelection(); + } + + private boolean mDrawMode = false; + public boolean getDrawMode() {return mDrawMode;} + public void onDrawMode() + { + mDrawMode = !mDrawMode; + mNoteMode = false; + clearSelection(); + onChangeSelection(); + } + + public void onDelete() + { + int numPages = getPageCount(); + for (int i = 0; i < numPages; i++) + { + DocPageView cv = (DocPageView) getOrCreateChild(i); + if (cv.hasInkAnnotationSelected()) + cv.deleteSelectedInkAnnotation(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) + { + if (mDrawMode) + { + float x = event.getX(); + float y = event.getY(); + switch (event.getAction()) + { + case MotionEvent.ACTION_DOWN: + touch_start(x, y); + break; + case MotionEvent.ACTION_MOVE: + touch_move(x, y); + break; + case MotionEvent.ACTION_UP: + touch_up(); + break; + } + + return true; + } + + return super.onTouchEvent(event); + } + + private float mX, mY; + private static final float TOUCH_TOLERANCE = 2; + + private int mCurrentInkLineColor = 0xFFFF0000; + public void setInkLineColor(int val) + { + mCurrentInkLineColor=val; + + // also change any selected annotation + if (hasInkAnnotationSelected()) + setSelectedInkLineColor(val); + } + public int getInkLineColor() {return mCurrentInkLineColor;} + + private float mCurrentInkLineThickness = 4.5f; + public float getInkLineThickness() {return mCurrentInkLineThickness;} + public void setInkLineThickness(float val) + { + mCurrentInkLineThickness=val; + + // also change any selected annotation + if (hasInkAnnotationSelected()) + setSelectedInkLineThickness(val); + } + + private void touch_start(float x, float y) + { + Point p = eventToScreen(x, y); + final DocPageView dpv = findPageViewContainingPoint(p.x, p.y, false); + if (dpv != null) + { + dpv.startDraw(p.x, p.y, mCurrentInkLineColor, mCurrentInkLineThickness); + } + + mX = x; + mY = y; + } + + private void touch_move(float x, float y) { + + float dx = Math.abs(x - mX); + float dy = Math.abs(y - mY); + if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) + { + Point p = eventToScreen(x, y); + final DocPageView dpv = findPageViewContainingPoint(p.x, p.y, false); + if (dpv != null) + { + dpv.continueDraw(p.x, p.y); + } + mX = x; + mY = y; + } + } + + private void touch_up() + { + // NOOP + } + } diff --git a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/LineWidthDialog.java b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/LineWidthDialog.java new file mode 100644 index 00000000..beea9d98 --- /dev/null +++ b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/LineWidthDialog.java @@ -0,0 +1,128 @@ +package com.artifex.mupdf.android; + +import android.content.Context; +import android.database.DataSetObserver; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.artifex.mupdf.fitz.R; + +import kankan.wheel.widget.OnWheelScrollListener; +import kankan.wheel.widget.WheelView; +import kankan.wheel.widget.adapters.WheelViewAdapter; + +public class LineWidthDialog +{ + private static final int POPUP_OFFSET = 30; + + private static final float values[] = new float[] {0.25f, 0.5f, 1, 1.5f, 3, 4.5f, 6, 8, 12, 18, 24}; + + public static void show(Context context, View anchor, float val, final WidthChangedListener listener) + { + float currentValue = val; + + View layout = View.inflate(context, R.layout.line_width_dialog, null); + + View wv = layout.findViewById(R.id.wheel); + WheelView wheel = (WheelView)wv; + + final LineWidthAdapter adapter = new LineWidthAdapter(context, values); + + wheel.setViewAdapter(adapter); + wheel.setVisibleItems(5); + + wheel.setCurrentItem(0); + for (int i=0;i<values.length;i++) + { + if (values[i] == currentValue) + wheel.setCurrentItem(i); + } + + wheel.addScrollingListener(new OnWheelScrollListener() { + @Override + public void onScrollingStarted(WheelView wheel) {} + + @Override + public void onScrollingFinished(WheelView wheel) { + listener.onWidthChanged(values[wheel.getCurrentItem()]); + } + }); + + // make a popup window + final PopupWindow popup = new PopupWindow(layout, + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + popup.setFocusable(true); + + // now show the popup + popup.showAsDropDown(anchor,POPUP_OFFSET,POPUP_OFFSET); + } + + public static class LineWidthAdapter implements WheelViewAdapter + { + public LineWidthAdapter(Context context, float values[]) { + super(); + mValues = values; + mContext = context; + } + + private Context mContext; + private float mValues[]; + + @Override + public View getItem(int position, View convertView, ViewGroup parent) + { + // make a View if needed. + if (convertView == null) + { + LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.line_width_item, parent, false); + } + + // get the value + float value = values[position]; + + // set the text string + TextView tv = ((TextView)convertView.findViewById(R.id.value)); + if(value == (int) value) + tv.setText(String.format("%d pt",(int)value)); + else + tv.setText(String.format("%s pt",value)); + + // adjust the height of the line + int h = (int)(value*3f/2f); + if (h<1) + h = 1; + View bar = ((View)convertView.findViewById(R.id.bar)); + bar.getLayoutParams().height = h; + bar.setLayoutParams(bar.getLayoutParams()); + + return convertView; + } + + @Override + public int getItemsCount() { + return mValues.length; + } + + @Override + public View getEmptyItem(View convertView, ViewGroup parent) { + return null; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + } + } + + public interface WidthChangedListener { + void onWidthChanged(float value); + } + +} diff --git a/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/ToolbarButton.java b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/ToolbarButton.java new file mode 100644 index 00000000..5760df48 --- /dev/null +++ b/platform/android/example/mupdf/src/main/java/com/artifex/mupdf/android/ToolbarButton.java @@ -0,0 +1,31 @@ +package com.artifex.mupdf.android; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.support.annotation.ColorInt; +import android.util.AttributeSet; +import android.widget.ImageButton; + +public class ToolbarButton extends ImageButton +{ + // Color.GRAY (0xFF888888) is too dark. Use something lighter. + @ColorInt + private static final int MYGRAY = 0xFFAAAAAA; + + public ToolbarButton(Context context) { + super(context); + } + + public ToolbarButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override public void setEnabled(boolean enabled) + { + super.setEnabled(enabled); + if (enabled) + setColorFilter(null); + else + setColorFilter(MYGRAY, PorterDuff.Mode.SRC_IN); + } +} diff --git a/platform/android/example/mupdf/src/main/res/drawable/colors.xml b/platform/android/example/mupdf/src/main/res/drawable/colors.xml new file mode 100644 index 00000000..eff1250e --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/colors.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <stroke android:width="1dp" android:color="#797979" /> + <solid android:color="@color/bluish_grey" /> + <corners + android:radius="7dp" /> + <padding + android:top="10dp" + android:left="10dp" + android:right="10dp" + android:bottom="10dp" + /> +</shape> diff --git a/platform/android/example/mupdf/src/main/res/drawable/icon_delete.xml b/platform/android/example/mupdf/src/main/res/drawable/icon_delete.xml new file mode 100755 index 00000000..d027b030 --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/icon_delete.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="50dp" + android:height="50dp" + android:viewportWidth="50" + android:viewportHeight="50"> + + <path + android:strokeColor="#333333" + android:strokeWidth="1.331" + android:pathData="M 25 9.92 C 33.3284540274 9.92 40.08 16.6715459726 40.08 25 C 40.08 33.3284540274 33.3284540274 40.08 25 40.08 C 16.6715459726 40.08 9.92 33.3284540274 9.92 25 C 9.92 16.6715459726 16.6715459726 9.92 25 9.92 Z" /> + <path + android:strokeColor="#333333" + android:strokeWidth="1.774" + android:pathData="M 17.622 32.934 L 32.257 18.299" /> + <path + android:strokeColor="#333333" + android:strokeWidth="1.774" + android:pathData="M 32.257 32.934 L 17.622 18.297" /> +</vector>
\ No newline at end of file diff --git a/platform/android/example/mupdf/src/main/res/drawable/icon_draw.xml b/platform/android/example/mupdf/src/main/res/drawable/icon_draw.xml new file mode 100644 index 00000000..808efdaa --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/icon_draw.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="50dp" + android:height="50dp" + android:viewportWidth="50" + android:viewportHeight="50"> + + <path + android:fillColor="#333333" + android:pathData="M33.1075,16.6386 L38.7232,19.2633 L31.7612,34.159 L26.1455,31.5343 +L33.1075,16.6386 Z" /> + <path + android:fillColor="#333333" + android:pathData="M25.735,32.435 L31.334,35.052 L25.884,39.634 Z" /> + <path + android:fillColor="#333333" + android:pathData="M39.29,18.067 L41.299,13.773 C41.358,13.646,41.302,13.495,41.176,13.435 +L36.02,11.025 C35.893,10.965,35.741,11.021,35.682,11.148 L33.675,15.442 +L39.29,18.067 Z" /> + <path + android:strokeColor="#333333" + android:strokeWidth="1.04" + android:strokeLineCap="round" + android:pathData="M8.738,10.366 C8.738,10.366,26.245,13.833,24.685,17.3 +C23.125,20.765,9.953,21.286,8.738,25.793 C7.525,30.3,24.859,39.313,24.859,39.313" /> +</vector>
\ No newline at end of file diff --git a/platform/android/example/mupdf/src/main/res/drawable/icon_highlight.xml b/platform/android/example/mupdf/src/main/res/drawable/icon_highlight.xml new file mode 100755 index 00000000..f42b88d9 --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/icon_highlight.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="50dp" + android:height="50dp" + android:viewportWidth="50" + android:viewportHeight="50"> + + <path + android:fillColor="#333333" + android:pathData="M 20.629 22.61 L 27.374 29.355 L 22.483 30.848 L 19.701 28.068 Z" /> + <path + android:fillColor="#333333" + android:pathData="M27.909,28.668l12.17-12.032c0.153-0.154,0.153-0.404,0.001-0.557l-6.199-6.251 +c-0.154-0.154-0.404-0.155-0.559-0.001L21.154,21.861L27.909,28.668z" /> + <path + android:fillColor="#333333" + android:pathData="M 18.992 28.629 L 14.841 32.359 L 20.726 32.404 L 21.803 31.594 Z" /> + <path + android:fillColor="#333333" + android:pathData="M 9.76 34.59 H 40.24 V 40.287 H 9.76 V 34.59 Z" /> +</vector>
\ No newline at end of file diff --git a/platform/android/example/mupdf/src/main/res/drawable/icon_line_color.xml b/platform/android/example/mupdf/src/main/res/drawable/icon_line_color.xml new file mode 100755 index 00000000..1c79066a --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/icon_line_color.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="50dp" + android:height="50dp" + android:viewportWidth="50" + android:viewportHeight="50"> + + <path + android:strokeColor="#81DBE7" + android:strokeWidth="1.56" + android:pathData="M40.797,35.303 +c0,1.518-1.229,2.747-2.747,2.747h-26.1c-1.516,0-2.747-1.229-2.747-2.747V14.698c0-1.518,1.231-2.748,2.747-2.748h26.1 +c1.519,0,2.747,1.23,2.747,2.748V35.303z" /> +</vector>
\ No newline at end of file diff --git a/platform/android/example/mupdf/src/main/res/drawable/icon_line_thickness.xml b/platform/android/example/mupdf/src/main/res/drawable/icon_line_thickness.xml new file mode 100755 index 00000000..1e48a6c9 --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/icon_line_thickness.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="50dp" + android:height="50dp" + android:viewportWidth="50" + android:viewportHeight="50"> + + <path + android:strokeColor="#333333" + android:strokeWidth="2.6" + android:pathData="M40.797,35.303 +c0,1.518-1.229,2.747-2.747,2.747h-26.1c-1.517,0-2.747-1.229-2.747-2.747V14.698c0-1.518,1.23-2.748,2.747-2.748h26.1 +c1.519,0,2.747,1.23,2.747,2.748V35.303z" /> +</vector>
\ No newline at end of file diff --git a/platform/android/example/mupdf/src/main/res/drawable/icon_note.xml b/platform/android/example/mupdf/src/main/res/drawable/icon_note.xml new file mode 100755 index 00000000..1f5732e3 --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/icon_note.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="50dp" + android:height="50dp" + android:viewportWidth="50" + android:viewportHeight="50"> + + <path + android:strokeColor="#333333" + android:strokeWidth="1.372" + android:strokeMiterLimit="10" + android:pathData="M12.204,32.945 +H10.09c-0.632,0-1.145-0.516-1.145-1.145V11.896c0-0.63,0.513-1.143,1.145-1.143h29.821c0.632,0,1.144,0.513,1.144,1.143v19.904 +c0,0.629-0.512,1.145-1.144,1.145H18.507l-6.303,6.301V32.945z" /> + <path + android:strokeColor="#333333" + android:strokeWidth="1.769" + android:strokeMiterLimit="10" + android:pathData="M 16.993 17.862 L 30.559 17.862" /> + <path + android:strokeColor="#333333" + android:strokeWidth="1.769" + android:strokeMiterLimit="10" + android:pathData="M 16.993 26.564 L 28.381 26.564" /> + <path + android:strokeColor="#333333" + android:strokeWidth="1.769" + android:strokeMiterLimit="10" + android:pathData="M 16.993 22.213 L 31.885 22.213" /> +</vector>
\ No newline at end of file diff --git a/platform/android/example/mupdf/src/main/res/drawable/number_format_popup.xml b/platform/android/example/mupdf/src/main/res/drawable/number_format_popup.xml new file mode 100644 index 00000000..5154168b --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/number_format_popup.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <stroke android:width="1dp" android:color="@color/formula_popup_background" /> + <solid android:color="@color/number_format_popup_background" /> + <corners + android:radius="7dp" /> + <padding + android:top="5dp" + android:left="5dp" + android:right="5dp" + android:bottom="5dp" + /> +</shape> diff --git a/platform/android/example/mupdf/src/main/res/drawable/toolbar_button2.xml b/platform/android/example/mupdf/src/main/res/drawable/toolbar_button2.xml new file mode 100644 index 00000000..d624e85f --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/toolbar_button2.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_selected="true"> + <shape> + <solid android:color="@color/white" /> + <corners android:radius="12dp"/> + <!--<padding android:left="10dp" android:top="10dp" android:right="10dp" android:bottom="10dp" />--> + </shape> + </item> + <item> + <shape> + <solid android:color="@color/button_normal" /> + <!--<padding android:left="10dp" android:top="10dp" android:right="10dp" android:bottom="10dp" />--> + </shape> + </item> +</selector> diff --git a/platform/android/example/mupdf/src/main/res/drawable/transparent_color_swatch.xml b/platform/android/example/mupdf/src/main/res/drawable/transparent_color_swatch.xml new file mode 100644 index 00000000..fdf3d369 --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/transparent_color_swatch.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item> + <shape> + <stroke android:color="@color/black" android:width="1dp" /> + <solid android:color="@color/white" /> + </shape> + </item> + + <item> + <rotate + android:fromDegrees="45"> + <shape + android:shape="line" > + <stroke android:color="@color/red" android:width="4dp" /> + </shape> + </rotate> + </item> + +</layer-list> diff --git a/platform/android/example/mupdf/src/main/res/drawable/wheel_bg.xml b/platform/android/example/mupdf/src/main/res/drawable/wheel_bg.xml new file mode 100644 index 00000000..f9fdd27a --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/wheel_bg.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + Android Wheel Control. + http://android-devblog.blogspot.com/2010/05/wheel-ui-contol.html + + Copyright 2010 Yuri Kanivets + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape android:shape="rectangle"> + <solid android:color="#ffffff" /> + <stroke android:width="1dp" android:color="#ffffff" /> + </shape> + </item> + +</layer-list>
\ No newline at end of file diff --git a/platform/android/example/mupdf/src/main/res/drawable/wheel_val.xml b/platform/android/example/mupdf/src/main/res/drawable/wheel_val.xml new file mode 100644 index 00000000..45f7f5c9 --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/drawable/wheel_val.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + Android Wheel Control. + http://android-devblog.blogspot.com/2010/05/wheel-ui-contol.html + + Copyright 2010 Yuri Kanivets + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#00ffffff"/> + <stroke android:width="1dp" + android:dashWidth="3px" + android:dashGap="3px" + android:color="#ff000000" /> +</shape> diff --git a/platform/android/example/mupdf/src/main/res/layout/annotate_toolbar.xml b/platform/android/example/mupdf/src/main/res/layout/annotate_toolbar.xml index 767a27b0..50814a44 100644 --- a/platform/android/example/mupdf/src/main/res/layout/annotate_toolbar.xml +++ b/platform/android/example/mupdf/src/main/res/layout/annotate_toolbar.xml @@ -19,27 +19,168 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" + android:gravity="center_horizontal" android:paddingLeft="10dp" android:paddingRight="10dp"> - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" android:layout_centerVertical="true" android:background="@drawable/toolbar_button" - android:id="@+id/show_annot_button" - android:src="@drawable/icon_toggle_annotations" /> + android:id="@+id/highlight_button" + android:src="@drawable/icon_highlight" /> <TextView - android:layout_width="match_parent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/black" + android:textSize="11sp" + android:text="HIGHLIGHT"/> + </LinearLayout> + + <!--a divider--> + <View + android:layout_width="1dp" + android:paddingRight="3dp" android:paddingLeft="3dp" + android:layout_height="match_parent" + android:background="#FF8E8F90" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:id="@+id/draw_tools_holder" + android:background="@drawable/toolbar_button2" + > + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + + <com.artifex.mupdf.android.ToolbarButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="fitXY" + android:layout_centerVertical="true" + android:background="@drawable/toolbar_button2" + android:id="@+id/draw_button" + android:src="@drawable/icon_draw" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/black" + android:textSize="11sp" + android:text="DRAW"/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + + <com.artifex.mupdf.android.ToolbarButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="fitXY" + android:layout_centerVertical="true" + android:background="@drawable/toolbar_button2" + android:id="@+id/line_color_button" + android:src="@drawable/icon_line_color" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/black" + android:textSize="11sp" + android:text=""/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + + <com.artifex.mupdf.android.ToolbarButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="fitXY" + android:layout_centerVertical="true" + android:background="@drawable/toolbar_button2" + android:id="@+id/line_thickness_button" + android:src="@drawable/icon_line_thickness" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/black" + android:textSize="11sp" + android:text=""/> + </LinearLayout> + + + </LinearLayout> + + <!--a divider--> + <View + android:layout_width="1dp" + android:paddingRight="3dp" android:paddingLeft="3dp" + android:layout_height="match_parent" + android:background="#FF8E8F90" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:id="@+id/note_holder" + android:background="@drawable/toolbar_button2" + > + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + + <com.artifex.mupdf.android.ToolbarButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="fitXY" + android:layout_centerVertical="true" + android:background="@drawable/toolbar_button2" + android:id="@+id/note_button" + android:src="@drawable/icon_note" /> + + <TextView + android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/black" android:textSize="11sp" - android:text="TOGGLE"/> + android:text="NOTE"/> </LinearLayout> + + </LinearLayout> + + <!--a divider--> <View android:layout_width="1dp" @@ -47,6 +188,78 @@ android:layout_height="match_parent" android:background="#FF8E8F90" /> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + + <com.artifex.mupdf.android.ToolbarButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="fitXY" + android:layout_centerVertical="true" + android:background="@drawable/toolbar_button" + android:id="@+id/delete_button" + android:src="@drawable/icon_delete" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/black" + android:textSize="11sp" + android:text="DELETE"/> + </LinearLayout> + + + <!-- + this layout holds some buttons for testing + normally it's not shown. + --> + + <LinearLayout + android:visibility="gone" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <!--a divider--> + <View + android:layout_width="1dp" + android:paddingRight="3dp" android:paddingLeft="3dp" + android:layout_height="match_parent" + android:background="#FF8E8F90" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + + <com.artifex.mupdf.android.ToolbarButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="fitXY" + android:layout_centerVertical="true" + android:background="@drawable/toolbar_button" + android:id="@+id/show_annot_button" + android:src="@drawable/icon_toggle_annotations" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/black" + android:textSize="11sp" + android:text="TOGGLE"/> + </LinearLayout> + + </LinearLayout> + </LinearLayout> </HorizontalScrollView> diff --git a/platform/android/example/mupdf/src/main/res/layout/colors.xml b/platform/android/example/mupdf/src/main/res/layout/colors.xml new file mode 100644 index 00000000..d7d7aa91 --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/layout/colors.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/colors" + android:orientation="vertical" + > + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <LinearLayout + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="false" + android:layout_alignParentTop="true" + android:id="@+id/font_panel"> + + <TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@color/bluish_grey" + android:textColor="#FFFFFFFF" + android:textSize="12pt" + android:id="@+id/color_dialog_title" /> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:id="@+id/fontcolors_row1" > + + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + + </LinearLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:id="@+id/fontcolors_row2"> + + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + + </LinearLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:id="@+id/fontcolors_row3"> + + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + <Button style="@style/font_color_button" android:background="#000000" android:tag="#000000"/> + + <Button style="@style/font_color_button" + android:id="@+id/transparent_color_button" + android:background="@drawable/transparent_color_swatch" + android:tag="transparent"/> + + </LinearLayout> + + </LinearLayout> + + </RelativeLayout> + + <!--<Button--> + <!--android:layout_width="wrap_content"--> + <!--android:layout_height="wrap_content"--> + <!--android:text="@string/done"--> + <!--android:id="@+id/done_button"--> + <!--android:background="@drawable/button"--> + <!--android:textColor="#FFFFFFFF"--> + <!--android:layout_gravity="right"/>--> + +</LinearLayout> diff --git a/platform/android/example/mupdf/src/main/res/layout/file_toolbar.xml b/platform/android/example/mupdf/src/main/res/layout/file_toolbar.xml index 6468c9d0..a35d2ed9 100644 --- a/platform/android/example/mupdf/src/main/res/layout/file_toolbar.xml +++ b/platform/android/example/mupdf/src/main/res/layout/file_toolbar.xml @@ -22,7 +22,7 @@ android:paddingLeft="10dp" android:paddingRight="10dp"> - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" @@ -47,7 +47,7 @@ android:paddingLeft="10dp" android:paddingRight="10dp"> - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" @@ -79,7 +79,7 @@ android:paddingLeft="10dp" android:paddingRight="10dp"> - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" @@ -104,7 +104,7 @@ android:paddingLeft="10dp" android:paddingRight="10dp"> - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" @@ -129,7 +129,7 @@ android:paddingLeft="10dp" android:paddingRight="10dp"> - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" diff --git a/platform/android/example/mupdf/src/main/res/layout/line_width_dialog.xml b/platform/android/example/mupdf/src/main/res/layout/line_width_dialog.xml new file mode 100644 index 00000000..5c47ab1d --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/layout/line_width_dialog.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@drawable/number_format_popup"> + + <kankan.wheel.widget.WheelView + android:layout_width="300dp" + android:layout_height="wrap_content" + android:id="@+id/wheel"> + + </kankan.wheel.widget.WheelView> + +</LinearLayout> diff --git a/platform/android/example/mupdf/src/main/res/layout/line_width_item.xml b/platform/android/example/mupdf/src/main/res/layout/line_width_item.xml new file mode 100644 index 00000000..0d960c7e --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/layout/line_width_item.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <View + android:id="@+id/bar" + android:layout_gravity="center_vertical" + android:layout_width="200dp" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:layout_height="1dp" + android:background="@color/black" /> + + <TextView + android:id="@+id/value" + android:layout_gravity="center_vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="10dp" + android:textColor="@color/black" + android:textSize="20sp"/> + +</LinearLayout> diff --git a/platform/android/example/mupdf/src/main/res/layout/pages_toolbar.xml b/platform/android/example/mupdf/src/main/res/layout/pages_toolbar.xml index 06ffa233..22e01a20 100644 --- a/platform/android/example/mupdf/src/main/res/layout/pages_toolbar.xml +++ b/platform/android/example/mupdf/src/main/res/layout/pages_toolbar.xml @@ -23,7 +23,7 @@ android:paddingLeft="10dp" android:paddingRight="10dp"> - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" @@ -50,7 +50,7 @@ android:paddingRight="10dp" > - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" @@ -85,7 +85,7 @@ android:paddingRight="10dp" > - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" diff --git a/platform/android/example/mupdf/src/main/res/layout/search_toolbar.xml b/platform/android/example/mupdf/src/main/res/layout/search_toolbar.xml index 973639bc..2b67e67d 100644 --- a/platform/android/example/mupdf/src/main/res/layout/search_toolbar.xml +++ b/platform/android/example/mupdf/src/main/res/layout/search_toolbar.xml @@ -63,7 +63,7 @@ android:gravity="center" > - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" @@ -88,7 +88,7 @@ android:gravity="center" > - <ImageButton + <com.artifex.mupdf.android.ToolbarButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitXY" diff --git a/platform/android/example/mupdf/src/main/res/values/strings.xml b/platform/android/example/mupdf/src/main/res/values/strings.xml index 3709da70..cc919a69 100644 --- a/platform/android/example/mupdf/src/main/res/values/strings.xml +++ b/platform/android/example/mupdf/src/main/res/values/strings.xml @@ -13,4 +13,7 @@ <string name="ok">OK</string> <string name="cancel">Cancel</string> + <string name="background">Background</string> + <string name="color">Color</string> + </resources> diff --git a/platform/android/example/mupdf/src/main/res/values/styles.xml b/platform/android/example/mupdf/src/main/res/values/styles.xml new file mode 100644 index 00000000..d767ff7f --- /dev/null +++ b/platform/android/example/mupdf/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- font color selection button. --> + <style name="font_color_button" parent="@android:style/Widget.Button"> + <item name="android:layout_height">20pt</item> + <item name="android:layout_width">20pt</item> + <item name="android:layout_margin">12dp</item> + </style> + +</resources> diff --git a/platform/java/src/kankan/wheel/widget/ItemsRange.java b/platform/java/src/kankan/wheel/widget/ItemsRange.java new file mode 100644 index 00000000..8b58a557 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/ItemsRange.java @@ -0,0 +1,81 @@ +/* + * Android Wheel Control. + * https://code.google.com/p/android-wheel/ + * + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +/** + * Range for visible items. + */ +public class ItemsRange { + // First item number + private int first; + + // Items count + private int count; + + /** + * Default constructor. Creates an empty range + */ + public ItemsRange() { + this(0, 0); + } + + /** + * Constructor + * @param first the number of first item + * @param count the count of items + */ + public ItemsRange(int first, int count) { + this.first = first; + this.count = count; + } + + /** + * Gets number of first item + * @return the number of the first item + */ + public int getFirst() { + return first; + } + + /** + * Gets number of last item + * @return the number of last item + */ + public int getLast() { + return getFirst() + getCount() - 1; + } + + /** + * Get items count + * @return the count of items + */ + public int getCount() { + return count; + } + + /** + * Tests whether item is contained by range + * @param index the item number + * @return true if item is contained + */ + public boolean contains(int index) { + return index >= getFirst() && index <= getLast(); + } +} diff --git a/platform/java/src/kankan/wheel/widget/OnWheelChangedListener.java b/platform/java/src/kankan/wheel/widget/OnWheelChangedListener.java new file mode 100644 index 00000000..baad897c --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/OnWheelChangedListener.java @@ -0,0 +1,33 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +/** + * Wheel changed listener interface. + * <p>The onChanged() method is called whenever current wheel positions is changed: + * <li> New Wheel position is set + * <li> Wheel view is scrolled + */ +public interface OnWheelChangedListener { + /** + * Callback method to be invoked when current item changed + * @param wheel the wheel view whose state has changed + * @param oldValue the old value of current item + * @param newValue the new value of current item + */ + void onChanged(WheelView wheel, int oldValue, int newValue); +} diff --git a/platform/java/src/kankan/wheel/widget/OnWheelClickedListener.java b/platform/java/src/kankan/wheel/widget/OnWheelClickedListener.java new file mode 100644 index 00000000..040bfc7b --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/OnWheelClickedListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +/** + * Wheel clicked listener interface. + * <p>The onItemClicked() method is called whenever a wheel item is clicked + * <li> New Wheel position is set + * <li> Wheel view is scrolled + */ +public interface OnWheelClickedListener { + /** + * Callback method to be invoked when current item clicked + * @param wheel the wheel view + * @param itemIndex the index of clicked item + */ + void onItemClicked(WheelView wheel, int itemIndex); +} diff --git a/platform/java/src/kankan/wheel/widget/OnWheelScrollListener.java b/platform/java/src/kankan/wheel/widget/OnWheelScrollListener.java new file mode 100644 index 00000000..8571a4fe --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/OnWheelScrollListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +/** + * Wheel scrolled listener interface. + */ +public interface OnWheelScrollListener { + /** + * Callback method to be invoked when scrolling started. + * @param wheel the wheel view whose state has changed. + */ + void onScrollingStarted(WheelView wheel); + + /** + * Callback method to be invoked when scrolling ended. + * @param wheel the wheel view whose state has changed. + */ + void onScrollingFinished(WheelView wheel); +} diff --git a/platform/java/src/kankan/wheel/widget/WheelAdapter.java b/platform/java/src/kankan/wheel/widget/WheelAdapter.java new file mode 100644 index 00000000..841c0b15 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/WheelAdapter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +/** + * Wheel adapter interface + * + * @deprecated Use WheelViewAdapter + */ +public interface WheelAdapter { + /** + * Gets items count + * @return the count of wheel items + */ + public int getItemsCount(); + + /** + * Gets a wheel item by index. + * + * @param index the item index + * @return the wheel item text or null + */ + public String getItem(int index); + + /** + * Gets maximum item length. It is used to determine the wheel width. + * If -1 is returned there will be used the default wheel width. + * + * @return the maximum item length or -1 + */ + public int getMaximumLength(); +} diff --git a/platform/java/src/kankan/wheel/widget/WheelRecycle.java b/platform/java/src/kankan/wheel/widget/WheelRecycle.java new file mode 100644 index 00000000..242fc89e --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/WheelRecycle.java @@ -0,0 +1,153 @@ +/* + * Android Wheel Control. + * https://code.google.com/p/android-wheel/ + * + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +import android.view.View; +import android.widget.LinearLayout; + +import java.util.LinkedList; +import java.util.List; + +/** + * Recycle stores wheel items to reuse. + */ +public class WheelRecycle { + // Cached items + private List<View> items; + + // Cached empty items + private List<View> emptyItems; + + // Wheel view + private WheelView wheel; + + /** + * Constructor + * @param wheel the wheel view + */ + public WheelRecycle(WheelView wheel) { + this.wheel = wheel; + } + + /** + * Recycles items from specified layout. + * There are saved only items not included to specified range. + * All the cached items are removed from original layout. + * + * @param layout the layout containing items to be cached + * @param firstItem the number of first item in layout + * @param range the range of current wheel items + * @return the new value of first item number + */ + public int recycleItems(LinearLayout layout, int firstItem, ItemsRange range) { + int index = firstItem; + for (int i = 0; i < layout.getChildCount();) { + if (!range.contains(index)) { + recycleView(layout.getChildAt(i), index); + layout.removeViewAt(i); + if (i == 0) { // first item + firstItem++; + } + } else { + i++; // go to next item + } + index++; + } + return firstItem; + } + + /** + * Gets item view + * @return the cached view + */ + public View getItem() { + return getCachedView(items); + } + + /** + * Gets empty item view + * @return the cached empty view + */ + public View getEmptyItem() { + return getCachedView(emptyItems); + } + + /** + * Clears all views + */ + public void clearAll() { + if (items != null) { + items.clear(); + } + if (emptyItems != null) { + emptyItems.clear(); + } + } + + /** + * Adds view to specified cache. Creates a cache list if it is null. + * @param view the view to be cached + * @param cache the cache list + * @return the cache list + */ + private List<View> addView(View view, List<View> cache) { + if (cache == null) { + cache = new LinkedList<View>(); + } + + cache.add(view); + return cache; + } + + /** + * Adds view to cache. Determines view type (item view or empty one) by index. + * @param view the view to be cached + * @param index the index of view + */ + private void recycleView(View view, int index) { + int count = wheel.getViewAdapter().getItemsCount(); + + if ((index < 0 || index >= count) && !wheel.isCyclic()) { + // empty view + emptyItems = addView(view, emptyItems); + } else { + while (index < 0) { + index = count + index; + } + index %= count; + items = addView(view, items); + } + } + + /** + * Gets view from specified cache. + * @param cache the cache + * @return the first view from cache. + */ + private View getCachedView(List<View> cache) { + if (cache != null && cache.size() > 0) { + View view = cache.get(0); + cache.remove(0); + return view; + } + return null; + } + +} diff --git a/platform/java/src/kankan/wheel/widget/WheelScroller.java b/platform/java/src/kankan/wheel/widget/WheelScroller.java new file mode 100644 index 00000000..0372da1a --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/WheelScroller.java @@ -0,0 +1,252 @@ +/* + * Android Wheel Control. + * https://code.google.com/p/android-wheel/ + * + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +/** + * Scroller class handles scrolling events and updates the + */ +public class WheelScroller { + /** + * Scrolling listener interface + */ + public interface ScrollingListener { + /** + * Scrolling callback called when scrolling is performed. + * @param distance the distance to scroll + */ + void onScroll(int distance); + + /** + * Starting callback called when scrolling is started + */ + void onStarted(); + + /** + * Finishing callback called after justifying + */ + void onFinished(); + + /** + * Justifying callback called to justify a view when scrolling is ended + */ + void onJustify(); + } + + /** Scrolling duration */ + private static final int SCROLLING_DURATION = 400; + + /** Minimum delta for scrolling */ + public static final int MIN_DELTA_FOR_SCROLLING = 1; + + // Listener + private ScrollingListener listener; + + // Context + private Context context; + + // Scrolling + private GestureDetector gestureDetector; + private Scroller scroller; + private int lastScrollY; + private float lastTouchedY; + private boolean isScrollingPerformed; + + /** + * Constructor + * @param context the current context + * @param listener the scrolling listener + */ + public WheelScroller(Context context, ScrollingListener listener) { + gestureDetector = new GestureDetector(context, gestureListener); + gestureDetector.setIsLongpressEnabled(false); + + scroller = new Scroller(context); + + this.listener = listener; + this.context = context; + } + + /** + * Set the the specified scrolling interpolator + * @param interpolator the interpolator + */ + public void setInterpolator(Interpolator interpolator) { + scroller.forceFinished(true); + scroller = new Scroller(context, interpolator); + } + + /** + * Scroll the wheel + * @param distance the scrolling distance + * @param time the scrolling duration + */ + public void scroll(int distance, int time) { + scroller.forceFinished(true); + + lastScrollY = 0; + + scroller.startScroll(0, 0, 0, distance, time != 0 ? time : SCROLLING_DURATION); + setNextMessage(MESSAGE_SCROLL); + + startScrolling(); + } + + /** + * Stops scrolling + */ + public void stopScrolling() { + scroller.forceFinished(true); + } + + /** + * Handles Touch event + * @param event the motion event + * @return + */ + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + lastTouchedY = event.getY(); + scroller.forceFinished(true); + clearMessages(); + break; + + case MotionEvent.ACTION_MOVE: + // perform scrolling + int distanceY = (int)(event.getY() - lastTouchedY); + if (distanceY != 0) { + startScrolling(); + listener.onScroll(distanceY); + lastTouchedY = event.getY(); + } + break; + } + + if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) { + justify(); + } + + return true; + } + + // gesture listener + private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() { + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + // Do scrolling in onTouchEvent() since onScroll() are not call immediately + // when user touch and move the wheel + return true; + } + + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + lastScrollY = 0; + final int maxY = 0x7FFFFFFF; + final int minY = -maxY; + scroller.fling(0, lastScrollY, 0, (int) -velocityY, 0, 0, minY, maxY); + setNextMessage(MESSAGE_SCROLL); + return true; + } + }; + + // Messages + private final int MESSAGE_SCROLL = 0; + private final int MESSAGE_JUSTIFY = 1; + + /** + * Set next message to queue. Clears queue before. + * + * @param message the message to set + */ + private void setNextMessage(int message) { + clearMessages(); + animationHandler.sendEmptyMessage(message); + } + + /** + * Clears messages from queue + */ + private void clearMessages() { + animationHandler.removeMessages(MESSAGE_SCROLL); + animationHandler.removeMessages(MESSAGE_JUSTIFY); + } + + // animation handler + private Handler animationHandler = new Handler() { + public void handleMessage(Message msg) { + scroller.computeScrollOffset(); + int currY = scroller.getCurrY(); + int delta = lastScrollY - currY; + lastScrollY = currY; + if (delta != 0) { + listener.onScroll(delta); + } + + // scrolling is not finished when it comes to final Y + // so, finish it manually + if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) { + currY = scroller.getFinalY(); + scroller.forceFinished(true); + } + if (!scroller.isFinished()) { + animationHandler.sendEmptyMessage(msg.what); + } else if (msg.what == MESSAGE_SCROLL) { + justify(); + } else { + finishScrolling(); + } + } + }; + + /** + * Justifies wheel + */ + private void justify() { + listener.onJustify(); + setNextMessage(MESSAGE_JUSTIFY); + } + + /** + * Starts scrolling + */ + private void startScrolling() { + if (!isScrollingPerformed) { + isScrollingPerformed = true; + listener.onStarted(); + } + } + + /** + * Finishes scrolling + */ + void finishScrolling() { + if (isScrollingPerformed) { + listener.onFinished(); + isScrollingPerformed = false; + } + } +} diff --git a/platform/java/src/kankan/wheel/widget/WheelView.java b/platform/java/src/kankan/wheel/widget/WheelView.java new file mode 100644 index 00000000..e8324e88 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/WheelView.java @@ -0,0 +1,889 @@ +/* + * Android Wheel Control. + * https://code.google.com/p/android-wheel/ + * + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.GradientDrawable.Orientation; +import android.media.AudioManager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.view.animation.Interpolator; +import android.widget.LinearLayout; + +import com.artifex.mupdf.fitz.R; + +import java.util.LinkedList; +import java.util.List; + +import kankan.wheel.widget.adapters.WheelViewAdapter; + + + +/** + * Numeric wheel view. + * + * @author Yuri Kanivets + */ +public class WheelView extends View { + + /** Top and bottom shadows colors */ + private static final int[] SHADOWS_COLORS = new int[] { 0xFF111111, + 0x00AAAAAA, 0x00AAAAAA }; + + /** Top and bottom items offset (to hide that) */ + private static final int ITEM_OFFSET_PERCENT = 10; + + /** Left and right padding value */ + private static final int PADDING = 10; + + /** Default count of visible items */ + private static final int DEF_VISIBLE_ITEMS = 5; + + // Wheel Values + private int currentItem = 0; + + // Count of visible items + private int visibleItems = DEF_VISIBLE_ITEMS; + + // Item height + private int itemHeight = 0; + + // Center Line + private Drawable centerDrawable; + + // // Shadows drawables + private GradientDrawable topShadow; + private GradientDrawable bottomShadow; + + // Scrolling + private WheelScroller scroller; + private boolean isScrollingPerformed; + private int scrollingOffset; + + // Cyclic + boolean isCyclic = false; + + // Items layout + private LinearLayout itemsLayout; + + // The number of first item in layout + private int firstItem; + + // View adapter + private WheelViewAdapter viewAdapter; + + // Recycle + private WheelRecycle recycle = new WheelRecycle(this); + + // Listeners + private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>(); + private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>(); + private List<OnWheelClickedListener> clickingListeners = new LinkedList<OnWheelClickedListener>(); + + /** + * Constructor + */ + public WheelView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initData(context); + } + + /** + * Constructor + */ + public WheelView(Context context, AttributeSet attrs) { + super(context, attrs); + initData(context); + } + + /** + * Constructor + */ + public WheelView(Context context) { + super(context); + initData(context); + } + + /** + * Initializes class data + * @param context the context + */ + private void initData(Context context) { + scroller = new WheelScroller(getContext(), scrollingListener); + } + + // Scrolling listener + WheelScroller.ScrollingListener scrollingListener = new WheelScroller.ScrollingListener() { + public void onStarted() { + isScrollingPerformed = true; + notifyScrollingListenersAboutStart(); + } + + public void onScroll(int distance) { + doScroll(distance); + + int height = getHeight(); + if (scrollingOffset > height) { + scrollingOffset = height; + scroller.stopScrolling(); + } else if (scrollingOffset < -height) { + scrollingOffset = -height; + scroller.stopScrolling(); + } + } + + public void onFinished() { + if (isScrollingPerformed) { + notifyScrollingListenersAboutEnd(); + isScrollingPerformed = false; + } + + scrollingOffset = 0; + invalidate(); + } + + public void onJustify() { + if (Math.abs(scrollingOffset) > WheelScroller.MIN_DELTA_FOR_SCROLLING) { + scroller.scroll(scrollingOffset, 0); + } + } + }; + + /** + * Set the the specified scrolling interpolator + * @param interpolator the interpolator + */ + public void setInterpolator(Interpolator interpolator) { + scroller.setInterpolator(interpolator); + } + + /** + * Gets count of visible items + * + * @return the count of visible items + */ + public int getVisibleItems() { + return visibleItems; + } + + /** + * Sets the desired count of visible items. + * Actual amount of visible items depends on wheel layout parameters. + * To apply changes and rebuild view call measure(). + * + * @param count the desired count for visible items + */ + public void setVisibleItems(int count) { + visibleItems = count; + } + + /** + * Gets view adapter + * @return the view adapter + */ + public WheelViewAdapter getViewAdapter() { + return viewAdapter; + } + + // Adapter listener + private DataSetObserver dataObserver = new DataSetObserver() { + @Override + public void onChanged() { + invalidateWheel(false); + } + + @Override + public void onInvalidated() { + invalidateWheel(true); + } + }; + + /** + * Sets view adapter. Usually new adapters contain different views, so + * it needs to rebuild view by calling measure(). + * + * @param viewAdapter the view adapter + */ + public void setViewAdapter(WheelViewAdapter viewAdapter) { + if (this.viewAdapter != null) { + this.viewAdapter.unregisterDataSetObserver(dataObserver); + } + this.viewAdapter = viewAdapter; + if (this.viewAdapter != null) { + this.viewAdapter.registerDataSetObserver(dataObserver); + } + + invalidateWheel(true); + } + + /** + * Adds wheel changing listener + * @param listener the listener + */ + public void addChangingListener(OnWheelChangedListener listener) { + changingListeners.add(listener); + } + + /** + * Removes wheel changing listener + * @param listener the listener + */ + public void removeChangingListener(OnWheelChangedListener listener) { + changingListeners.remove(listener); + } + + /** + * Notifies changing listeners + * @param oldValue the old wheel value + * @param newValue the new wheel value + */ + protected void notifyChangingListeners(int oldValue, int newValue) { + for (OnWheelChangedListener listener : changingListeners) { + listener.onChanged(this, oldValue, newValue); + } + + // play a sound. + // TODO: something better + this.playSoundEffect(SoundEffectConstants.CLICK); + } + + /** + * Adds wheel scrolling listener + * @param listener the listener + */ + public void addScrollingListener(OnWheelScrollListener listener) { + scrollingListeners.add(listener); + } + + /** + * Removes wheel scrolling listener + * @param listener the listener + */ + public void removeScrollingListener(OnWheelScrollListener listener) { + scrollingListeners.remove(listener); + } + + /** + * Notifies listeners about starting scrolling + */ + protected void notifyScrollingListenersAboutStart() { + for (OnWheelScrollListener listener : scrollingListeners) { + listener.onScrollingStarted(this); + } + } + + /** + * Notifies listeners about ending scrolling + */ + protected void notifyScrollingListenersAboutEnd() { + for (OnWheelScrollListener listener : scrollingListeners) { + listener.onScrollingFinished(this); + } + } + + /** + * Adds wheel clicking listener + * @param listener the listener + */ + public void addClickingListener(OnWheelClickedListener listener) { + clickingListeners.add(listener); + } + + /** + * Removes wheel clicking listener + * @param listener the listener + */ + public void removeClickingListener(OnWheelClickedListener listener) { + clickingListeners.remove(listener); + } + + /** + * Notifies listeners about clicking + */ + protected void notifyClickListenersAboutClick(int item) { + for (OnWheelClickedListener listener : clickingListeners) { + listener.onItemClicked(this, item); + } + } + + /** + * Gets current value + * + * @return the current value + */ + public int getCurrentItem() { + return currentItem; + } + + /** + * Sets the current item. Does nothing when index is wrong. + * + * @param index the item index + * @param animated the animation flag + */ + public void setCurrentItem(int index, boolean animated) { + if (viewAdapter == null || viewAdapter.getItemsCount() == 0) { + return; // throw? + } + + int itemCount = viewAdapter.getItemsCount(); + if (index < 0 || index >= itemCount) { + if (isCyclic) { + while (index < 0) { + index += itemCount; + } + index %= itemCount; + } else{ + return; // throw? + } + } + if (index != currentItem) { + if (animated) { + int itemsToScroll = index - currentItem; + if (isCyclic) { + int scroll = itemCount + Math.min(index, currentItem) - Math.max(index, currentItem); + if (scroll < Math.abs(itemsToScroll)) { + itemsToScroll = itemsToScroll < 0 ? scroll : -scroll; + } + } + scroll(itemsToScroll, 0); + } else { + scrollingOffset = 0; + + int old = currentItem; + currentItem = index; + + notifyChangingListeners(old, currentItem); + + invalidate(); + } + } + } + + /** + * Sets the current item w/o animation. Does nothing when index is wrong. + * + * @param index the item index + */ + public void setCurrentItem(int index) { + setCurrentItem(index, false); + } + + /** + * Tests if wheel is cyclic. That means before the 1st item there is shown the last one + * @return true if wheel is cyclic + */ + public boolean isCyclic() { + return isCyclic; + } + + /** + * Set wheel cyclic flag + * @param isCyclic the flag to set + */ + public void setCyclic(boolean isCyclic) { + this.isCyclic = isCyclic; + invalidateWheel(false); + } + + /** + * Invalidates wheel + * @param clearCaches if true then cached views will be clear + */ + public void invalidateWheel(boolean clearCaches) { + if (clearCaches) { + recycle.clearAll(); + if (itemsLayout != null) { + itemsLayout.removeAllViews(); + } + scrollingOffset = 0; + } else if (itemsLayout != null) { + // cache all items + recycle.recycleItems(itemsLayout, firstItem, new ItemsRange()); + } + + invalidate(); + } + + /** + * Initializes resources + */ + private void initResourcesIfNecessary() { + if (centerDrawable == null) { + centerDrawable = getContext().getResources().getDrawable(R.drawable.wheel_val); + } + + if (topShadow == null) { + topShadow = new GradientDrawable(Orientation.TOP_BOTTOM, SHADOWS_COLORS); + } + + if (bottomShadow == null) { + bottomShadow = new GradientDrawable(Orientation.BOTTOM_TOP, SHADOWS_COLORS); + } + + setBackgroundResource(R.drawable.wheel_bg); + } + + /** + * Calculates desired height for layout + * + * @param layout + * the source layout + * @return the desired layout height + */ + private int getDesiredHeight(LinearLayout layout) { + if (layout != null && layout.getChildAt(0) != null) { + itemHeight = layout.getChildAt(0).getMeasuredHeight(); + } + + int desired = itemHeight * visibleItems - itemHeight * ITEM_OFFSET_PERCENT / 50; + + return Math.max(desired, getSuggestedMinimumHeight()); + } + + /** + * Returns height of wheel item + * @return the item height + */ + private int getItemHeight() { + if (itemHeight != 0) { + return itemHeight; + } + + if (itemsLayout != null && itemsLayout.getChildAt(0) != null) { + itemHeight = itemsLayout.getChildAt(0).getHeight(); + return itemHeight; + } + + return getHeight() / visibleItems; + } + + /** + * Calculates control width and creates text layouts + * @param widthSize the input layout width + * @param mode the layout mode + * @return the calculated control width + */ + private int calculateLayoutWidth(int widthSize, int mode) { + initResourcesIfNecessary(); + + // TODO: make it static + itemsLayout.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + itemsLayout.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int width = itemsLayout.getMeasuredWidth(); + + if (mode == MeasureSpec.EXACTLY) { + width = widthSize; + } else { + width += 2 * PADDING; + + // Check against our minimum width + width = Math.max(width, getSuggestedMinimumWidth()); + + if (mode == MeasureSpec.AT_MOST && widthSize < width) { + width = widthSize; + } + } + + itemsLayout.measure(MeasureSpec.makeMeasureSpec(width - 2 * PADDING, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + + return width; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + buildViewForMeasuring(); + + int width = calculateLayoutWidth(widthSize, widthMode); + + int height; + if (heightMode == MeasureSpec.EXACTLY) { + height = heightSize; + } else { + height = getDesiredHeight(itemsLayout); + + if (heightMode == MeasureSpec.AT_MOST) { + height = Math.min(height, heightSize); + } + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + layout(r - l, b - t); + } + + /** + * Sets layouts width and height + * @param width the layout width + * @param height the layout height + */ + private void layout(int width, int height) { + int itemsWidth = width - 2 * PADDING; + + itemsLayout.layout(0, 0, itemsWidth, height); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (viewAdapter != null && viewAdapter.getItemsCount() > 0) { + updateView(); + + drawItems(canvas); + drawCenterRect(canvas); + } + +// drawShadows(canvas); + } + + /** + * Draws shadows on top and bottom of control + * @param canvas the canvas for drawing + */ + private void drawShadows(Canvas canvas) { + int height = (int)(1.5 * getItemHeight()); + topShadow.setBounds(0, 0, getWidth(), height); + topShadow.draw(canvas); + + bottomShadow.setBounds(0, getHeight() - height, getWidth(), getHeight()); + bottomShadow.draw(canvas); + } + + /** + * Draws items + * @param canvas the canvas for drawing + */ + private void drawItems(Canvas canvas) { + canvas.save(); + + int top = (currentItem - firstItem) * getItemHeight() + (getItemHeight() - getHeight()) / 2; + canvas.translate(PADDING, - top + scrollingOffset); + + itemsLayout.draw(canvas); + + canvas.restore(); + } + + /** + * Draws rect for current value + * @param canvas the canvas for drawing + */ + private void drawCenterRect(Canvas canvas) { + int center = getHeight() / 2; + int offset = (int) (getItemHeight() / 2 * 1.2); + centerDrawable.setBounds(0, center - offset, getWidth(), center + offset); + centerDrawable.draw(canvas); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled() || getViewAdapter() == null) { + return true; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_MOVE: + if (getParent() != null) { + getParent().requestDisallowInterceptTouchEvent(true); + } + break; + + case MotionEvent.ACTION_UP: + if (!isScrollingPerformed) { + int distance = (int) event.getY() - getHeight() / 2; + if (distance > 0) { + distance += getItemHeight() / 2; + } else { + distance -= getItemHeight() / 2; + } + int items = distance / getItemHeight(); + if (items != 0 && isValidItemIndex(currentItem + items)) { + notifyClickListenersAboutClick(currentItem + items); + } + } + break; + } + + return scroller.onTouchEvent(event); + } + + /** + * Scrolls the wheel + * @param delta the scrolling value + */ + private void doScroll(int delta) { + scrollingOffset += delta; + + int itemHeight = getItemHeight(); + int count = scrollingOffset / itemHeight; + + int pos = currentItem - count; + int itemCount = viewAdapter.getItemsCount(); + + int fixPos = scrollingOffset % itemHeight; + if (Math.abs(fixPos) <= itemHeight / 2) { + fixPos = 0; + } + if (isCyclic && itemCount > 0) { + if (fixPos > 0) { + pos--; + count++; + } else if (fixPos < 0) { + pos++; + count--; + } + // fix position by rotating + while (pos < 0) { + pos += itemCount; + } + pos %= itemCount; + } else { + // + if (pos < 0) { + count = currentItem; + pos = 0; + } else if (pos >= itemCount) { + count = currentItem - itemCount + 1; + pos = itemCount - 1; + } else if (pos > 0 && fixPos > 0) { + pos--; + count++; + } else if (pos < itemCount - 1 && fixPos < 0) { + pos++; + count--; + } + } + + int offset = scrollingOffset; + if (pos != currentItem) { + setCurrentItem(pos, false); + } else { + invalidate(); + } + + // update offset + scrollingOffset = offset - count * itemHeight; + if (scrollingOffset > getHeight()) { + scrollingOffset = scrollingOffset % getHeight() + getHeight(); + } + } + + /** + * Scroll the wheel + * @param itemsToSkip items to scroll + * @param time scrolling duration + */ + public void scroll(int itemsToScroll, int time) { + int distance = itemsToScroll * getItemHeight() - scrollingOffset; + scroller.scroll(distance, time); + } + + /** + * Calculates range for wheel items + * @return the items range + */ + private ItemsRange getItemsRange() { + if (getItemHeight() == 0) { + return null; + } + + int first = currentItem; + int count = 1; + + while (count * getItemHeight() < getHeight()) { + first--; + count += 2; // top + bottom items + } + + if (scrollingOffset != 0) { + if (scrollingOffset > 0) { + first--; + } + count++; + + // process empty items above the first or below the second + int emptyItems = scrollingOffset / getItemHeight(); + first -= emptyItems; + count += Math.asin(emptyItems); + } + return new ItemsRange(first, count); + } + + /** + * Rebuilds wheel items if necessary. Caches all unused items. + * + * @return true if items are rebuilt + */ + private boolean rebuildItems() { + boolean updated = false; + ItemsRange range = getItemsRange(); + if (itemsLayout != null) { + int first = recycle.recycleItems(itemsLayout, firstItem, range); + updated = firstItem != first; + firstItem = first; + } else { + createItemsLayout(); + updated = true; + } + + if (!updated) { + updated = firstItem != range.getFirst() || itemsLayout.getChildCount() != range.getCount(); + } + + if (firstItem > range.getFirst() && firstItem <= range.getLast()) { + for (int i = firstItem - 1; i >= range.getFirst(); i--) { + if (!addViewItem(i, true)) { + break; + } + firstItem = i; + } + } else { + firstItem = range.getFirst(); + } + + int first = firstItem; + for (int i = itemsLayout.getChildCount(); i < range.getCount(); i++) { + if (!addViewItem(firstItem + i, false) && itemsLayout.getChildCount() == 0) { + first++; + } + } + firstItem = first; + + return updated; + } + + /** + * Updates view. Rebuilds items and label if necessary, recalculate items sizes. + */ + private void updateView() { + if (rebuildItems()) { + calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY); + layout(getWidth(), getHeight()); + } + } + + /** + * Creates item layouts if necessary + */ + private void createItemsLayout() { + if (itemsLayout == null) { + itemsLayout = new LinearLayout(getContext()); + itemsLayout.setOrientation(LinearLayout.VERTICAL); + } + } + + /** + * Builds view for measuring + */ + private void buildViewForMeasuring() { + // clear all items + if (itemsLayout != null) { + recycle.recycleItems(itemsLayout, firstItem, new ItemsRange()); + } else { + createItemsLayout(); + } + + // add views + int addItems = visibleItems / 2; + for (int i = currentItem + addItems; i >= currentItem - addItems; i--) { + if (addViewItem(i, true)) { + firstItem = i; + } + } + } + + /** + * Adds view for item to items layout + * @param index the item index + * @param first the flag indicates if view should be first + * @return true if corresponding item exists and is added + */ + private boolean addViewItem(int index, boolean first) { + View view = getItemView(index); + if (view != null) { + if (first) { + itemsLayout.addView(view, 0); + } else { + itemsLayout.addView(view); + } + + return true; + } + + return false; + } + + /** + * Checks whether intem index is valid + * @param index the item index + * @return true if item index is not out of bounds or the wheel is cyclic + */ + private boolean isValidItemIndex(int index) { + return viewAdapter != null && viewAdapter.getItemsCount() > 0 && + (isCyclic || index >= 0 && index < viewAdapter.getItemsCount()); + } + + /** + * Returns view for specified item + * @param index the item index + * @return item view or empty view if index is out of bounds + */ + private View getItemView(int index) { + if (viewAdapter == null || viewAdapter.getItemsCount() == 0) { + return null; + } + int count = viewAdapter.getItemsCount(); + if (!isValidItemIndex(index)) { + return viewAdapter.getEmptyItem(recycle.getEmptyItem(), itemsLayout); + } else { + while (index < 0) { + index = count + index; + } + } + + index %= count; + return viewAdapter.getItem(index, recycle.getItem(), itemsLayout); + } + + /** + * Stops scrolling + */ + public void stopScrolling() { + scroller.stopScrolling(); + } +} diff --git a/platform/java/src/kankan/wheel/widget/adapters/AbstractWheelAdapter.java b/platform/java/src/kankan/wheel/widget/adapters/AbstractWheelAdapter.java new file mode 100644 index 00000000..8d7f1447 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/adapters/AbstractWheelAdapter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget.adapters; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; + +import java.util.LinkedList; +import java.util.List; + +/** + * Abstract Wheel adapter. + */ +public abstract class AbstractWheelAdapter implements WheelViewAdapter { + // Observers + private List<DataSetObserver> datasetObservers; + + @Override + public View getEmptyItem(View convertView, ViewGroup parent) { + return null; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + if (datasetObservers == null) { + datasetObservers = new LinkedList<DataSetObserver>(); + } + datasetObservers.add(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + if (datasetObservers != null) { + datasetObservers.remove(observer); + } + } + + /** + * Notifies observers about data changing + */ + protected void notifyDataChangedEvent() { + if (datasetObservers != null) { + for (DataSetObserver observer : datasetObservers) { + observer.onChanged(); + } + } + } + + /** + * Notifies observers about invalidating data + */ + protected void notifyDataInvalidatedEvent() { + if (datasetObservers != null) { + for (DataSetObserver observer : datasetObservers) { + observer.onInvalidated(); + } + } + } +} diff --git a/platform/java/src/kankan/wheel/widget/adapters/AbstractWheelTextAdapter.java b/platform/java/src/kankan/wheel/widget/adapters/AbstractWheelTextAdapter.java new file mode 100644 index 00000000..620c8e74 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/adapters/AbstractWheelTextAdapter.java @@ -0,0 +1,268 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kankan.wheel.widget.adapters; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +/** + * Abstract wheel adapter provides common functionality for adapters. + */ +public abstract class AbstractWheelTextAdapter extends AbstractWheelAdapter { + + /** Text view resource. Used as a default view for adapter. */ + public static final int TEXT_VIEW_ITEM_RESOURCE = -1; + + /** No resource constant. */ + protected static final int NO_RESOURCE = 0; + + /** Default text color */ + public static final int DEFAULT_TEXT_COLOR = 0xFF101010; + + /** Default text color */ + public static final int LABEL_COLOR = 0xFF700070; + + /** Default text size */ + public static final int DEFAULT_TEXT_SIZE = 24; + + // Text settings + private int textColor = DEFAULT_TEXT_COLOR; + private int textSize = DEFAULT_TEXT_SIZE; + + // Current context + protected Context context; + // Layout inflater + protected LayoutInflater inflater; + + // Items resources + protected int itemResourceId; + protected int itemTextResourceId; + + // Empty items resources + protected int emptyItemResourceId; + + /** + * Constructor + * @param context the current context + */ + protected AbstractWheelTextAdapter(Context context) { + this(context, TEXT_VIEW_ITEM_RESOURCE); + } + + /** + * Constructor + * @param context the current context + * @param itemResource the resource ID for a layout file containing a TextView to use when instantiating items views + */ + protected AbstractWheelTextAdapter(Context context, int itemResource) { + this(context, itemResource, NO_RESOURCE); + } + + /** + * Constructor + * @param context the current context + * @param itemResource the resource ID for a layout file containing a TextView to use when instantiating items views + * @param itemTextResource the resource ID for a text view in the item layout + */ + protected AbstractWheelTextAdapter(Context context, int itemResource, int itemTextResource) { + this.context = context; + itemResourceId = itemResource; + itemTextResourceId = itemTextResource; + + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Gets text color + * @return the text color + */ + public int getTextColor() { + return textColor; + } + + /** + * Sets text color + * @param textColor the text color to set + */ + public void setTextColor(int textColor) { + this.textColor = textColor; + } + + /** + * Gets text size + * @return the text size + */ + public int getTextSize() { + return textSize; + } + + /** + * Sets text size + * @param textSize the text size to set + */ + public void setTextSize(int textSize) { + this.textSize = textSize; + } + + /** + * Gets resource Id for items views + * @return the item resource Id + */ + public int getItemResource() { + return itemResourceId; + } + + /** + * Sets resource Id for items views + * @param itemResourceId the resource Id to set + */ + public void setItemResource(int itemResourceId) { + this.itemResourceId = itemResourceId; + } + + /** + * Gets resource Id for text view in item layout + * @return the item text resource Id + */ + public int getItemTextResource() { + return itemTextResourceId; + } + + /** + * Sets resource Id for text view in item layout + * @param itemTextResourceId the item text resource Id to set + */ + public void setItemTextResource(int itemTextResourceId) { + this.itemTextResourceId = itemTextResourceId; + } + + /** + * Gets resource Id for empty items views + * @return the empty item resource Id + */ + public int getEmptyItemResource() { + return emptyItemResourceId; + } + + /** + * Sets resource Id for empty items views + * @param emptyItemResourceId the empty item resource Id to set + */ + public void setEmptyItemResource(int emptyItemResourceId) { + this.emptyItemResourceId = emptyItemResourceId; + } + + + /** + * Returns text for specified item + * @param index the item index + * @return the text of specified items + */ + protected abstract CharSequence getItemText(int index); + + @Override + public View getItem(int index, View convertView, ViewGroup parent) { + if (index >= 0 && index < getItemsCount()) { + if (convertView == null) { + convertView = getView(itemResourceId, parent); + } + TextView textView = getTextView(convertView, itemTextResourceId); + if (textView != null) { + CharSequence text = getItemText(index); + if (text == null) { + text = ""; + } + textView.setText(text); + + if (itemResourceId == TEXT_VIEW_ITEM_RESOURCE) { + configureTextView(textView); + } + } + return convertView; + } + return null; + } + + @Override + public View getEmptyItem(View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = getView(emptyItemResourceId, parent); + } + if (emptyItemResourceId == TEXT_VIEW_ITEM_RESOURCE && convertView instanceof TextView) { + configureTextView((TextView)convertView); + } + + return convertView; + } + + /** + * Configures text view. Is called for the TEXT_VIEW_ITEM_RESOURCE views. + * @param view the text view to be configured + */ + protected void configureTextView(TextView view) { + view.setTextColor(textColor); + view.setGravity(Gravity.CENTER); + view.setTextSize(textSize); + view.setLines(1); +// view.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD); + view.setTypeface(Typeface.SANS_SERIF, Typeface.NORMAL); + } + + /** + * Loads a text view from view + * @param view the text view or layout containing it + * @param textResource the text resource Id in layout + * @return the loaded text view + */ + private TextView getTextView(View view, int textResource) { + TextView text = null; + try { + if (textResource == NO_RESOURCE && view instanceof TextView) { + text = (TextView) view; + } else if (textResource != NO_RESOURCE) { + text = (TextView) view.findViewById(textResource); + } + } catch (ClassCastException e) { + Log.e("AbstractWheelAdapter", "You must supply a resource ID for a TextView"); + throw new IllegalStateException( + "AbstractWheelAdapter requires the resource ID to be a TextView", e); + } + + return text; + } + + /** + * Loads view from resources + * @param resource the resource Id + * @return the loaded view or null if resource is not set + */ + private View getView(int resource, ViewGroup parent) { + switch (resource) { + case NO_RESOURCE: + return null; + case TEXT_VIEW_ITEM_RESOURCE: + return new TextView(context); + default: + return inflater.inflate(resource, parent, false); + } + } +} diff --git a/platform/java/src/kankan/wheel/widget/adapters/AdapterWheel.java b/platform/java/src/kankan/wheel/widget/adapters/AdapterWheel.java new file mode 100644 index 00000000..000001cd --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/adapters/AdapterWheel.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget.adapters; + +import android.content.Context; + +import kankan.wheel.widget.WheelAdapter; + +/** + * Adapter class for old wheel adapter (deprecated WheelAdapter class). + * + * @deprecated Will be removed soon + */ +public class AdapterWheel extends AbstractWheelTextAdapter { + + // Source adapter + private WheelAdapter adapter; + + /** + * Constructor + * @param context the current context + * @param adapter the source adapter + */ + public AdapterWheel(Context context, WheelAdapter adapter) { + super(context); + + this.adapter = adapter; + } + + /** + * Gets original adapter + * @return the original adapter + */ + public WheelAdapter getAdapter() { + return adapter; + } + + @Override + public int getItemsCount() { + return adapter.getItemsCount(); + } + + @Override + protected CharSequence getItemText(int index) { + return adapter.getItem(index); + } + +} diff --git a/platform/java/src/kankan/wheel/widget/adapters/ArrayWheelAdapter.java b/platform/java/src/kankan/wheel/widget/adapters/ArrayWheelAdapter.java new file mode 100644 index 00000000..c9d430d7 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/adapters/ArrayWheelAdapter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kankan.wheel.widget.adapters; + +import android.content.Context; + +/** + * The simple Array wheel adapter + * @param <T> the element type + */ +public class ArrayWheelAdapter<T> extends AbstractWheelTextAdapter { + + // items + private T items[]; + + /** + * Constructor + * @param context the current context + * @param items the items + */ + public ArrayWheelAdapter(Context context, T items[]) { + super(context); + + //setEmptyItemResource(TEXT_VIEW_ITEM_RESOURCE); + this.items = items; + } + + public void setItems(T items[]) + { + this.items = items; + } + + @Override + public CharSequence getItemText(int index) { + if (index >= 0 && index < items.length) { + T item = items[index]; + if (item instanceof CharSequence) { + return (CharSequence) item; + } + return item.toString(); + } + return null; + } + + @Override + public int getItemsCount() { + return items.length; + } +} diff --git a/platform/java/src/kankan/wheel/widget/adapters/ArrayWheelAdapterColor.java b/platform/java/src/kankan/wheel/widget/adapters/ArrayWheelAdapterColor.java new file mode 100644 index 00000000..cf4a9954 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/adapters/ArrayWheelAdapterColor.java @@ -0,0 +1,50 @@ +package kankan.wheel.widget.adapters; + +import android.content.Context; +import android.graphics.Color; +import android.widget.TextView; + +public class ArrayWheelAdapterColor<T> extends AbstractWheelTextAdapter { + + // items + private T items[]; + + public ArrayWheelAdapterColor(Context context, T items[]) { + super(context); + + this.items = items; + } + + @Override + public CharSequence getItemText(int index) { + if (index >= 0 && index < items.length) { + T item = items[index]; + if (item instanceof CharSequence) { + return (CharSequence) item; + } + return item.toString(); + } + return null; + } + + @Override + public int getItemsCount() { + return items.length; + } + + @Override + protected void configureTextView(TextView view) { + super.configureTextView(view); + + // if the text ends with "(red)" + // color it red. + String text = view.getText().toString(); + if (text.endsWith("(red)")) + { + text = text.replace("(red)",""); + view.setText(text); + view.setTextColor(Color.parseColor("#ff0000")); + } + } + +}
\ No newline at end of file diff --git a/platform/java/src/kankan/wheel/widget/adapters/NumericWheelAdapter.java b/platform/java/src/kankan/wheel/widget/adapters/NumericWheelAdapter.java new file mode 100644 index 00000000..ec8b9c86 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/adapters/NumericWheelAdapter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget.adapters; + +import android.content.Context; + +/** + * Numeric Wheel adapter. + */ +public class NumericWheelAdapter extends AbstractWheelTextAdapter { + + /** The default min value */ + public static final int DEFAULT_MAX_VALUE = 9; + + /** The default max value */ + private static final int DEFAULT_MIN_VALUE = 0; + + // Values + private int minValue; + private int maxValue; + + // format + private String format; + + /** + * Constructor + * @param context the current context + */ + public NumericWheelAdapter(Context context) { + this(context, DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE); + } + + /** + * Constructor + * @param context the current context + * @param minValue the wheel min value + * @param maxValue the wheel max value + */ + public NumericWheelAdapter(Context context, int minValue, int maxValue) { + this(context, minValue, maxValue, null); + } + + /** + * Constructor + * @param context the current context + * @param minValue the wheel min value + * @param maxValue the wheel max value + * @param format the format string + */ + public NumericWheelAdapter(Context context, int minValue, int maxValue, String format) { + super(context); + + this.minValue = minValue; + this.maxValue = maxValue; + this.format = format; + } + + @Override + public CharSequence getItemText(int index) { + if (index >= 0 && index < getItemsCount()) { + int value = minValue + index; + return format != null ? String.format(format, value) : Integer.toString(value); + } + return null; + } + + @Override + public int getItemsCount() { + return maxValue - minValue + 1; + } +} diff --git a/platform/java/src/kankan/wheel/widget/adapters/WheelViewAdapter.java b/platform/java/src/kankan/wheel/widget/adapters/WheelViewAdapter.java new file mode 100644 index 00000000..db256570 --- /dev/null +++ b/platform/java/src/kankan/wheel/widget/adapters/WheelViewAdapter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2011 Yuri Kanivets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kankan.wheel.widget.adapters; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; + +/** + * Wheel items adapter interface + */ +public interface WheelViewAdapter { + /** + * Gets items count + * @return the count of wheel items + */ + public int getItemsCount(); + + /** + * Get a View that displays the data at the specified position in the data set + * + * @param index the item index + * @param convertView the old view to reuse if possible + * @param parent the parent that this view will eventually be attached to + * @return the wheel item View + */ + public View getItem(int index, View convertView, ViewGroup parent); + + /** + * Get a View that displays an empty wheel item placed before the first or after + * the last wheel item. + * + * @param convertView the old view to reuse if possible + * @param parent the parent that this view will eventually be attached to + * @return the empty item View + */ + public View getEmptyItem(View convertView, ViewGroup parent); + + /** + * Register an observer that is called when changes happen to the data used by this adapter. + * @param observer the observer to be registered + */ + public void registerDataSetObserver(DataSetObserver observer); + + /** + * Unregister an observer that has previously been registered + * @param observer the observer to be unregistered + */ + void unregisterDataSetObserver (DataSetObserver observer); +} |