Dragging Images When Scaling Must Be Restricted

January 21, 2014 Leave a comment

I recently retired, but I have one more little tip to blog about. While I have a few ideas for some apps, I doubt that I’ll have to do the kind of intensive problem solving required during my job. Therefore this might be the last post.

I was involved with a suite of clients for business intelligence. The primary clients were created with Adobe Flex and ran in the browser. They provided for creating and viewing reports. The iOS and Android clients provided for viewing reports. Thus features were implemented in the Flex product first, and we who supported the mobile clients had to cope with adding them. The feature relevant to this blog entry was the ability to specify numerous scaling options for images (e.g. photos) that could be incorporated into reports. Some of these scaling options had no natural analog to the Android scaling options for images.

To support the requirement for panning and zooming images I took full advantage of the PhotoView library provided by Chris Banes. This library was a great solution for all but two of the required scaling options. Our product allowed for two rather silly options of fitting an image to the width or to the height of the viewport that the report designer drew on screen. If the other dimension of the image was greater, then part of the image would be invisible. I had to provide support for letting the user drag the image around in the viewport so that all of it could be seen.

The PhotoView library would have handled this except for the fact that we needed to set the scale type on the ImageView class to MATRIX, and PhotoView does not allow that. With no natural analogous scaling type to our “fit width” and “fit height”, I had to create a new subclass of ImageView to handle just the images requiring those types. The ReportImageView class has some code for doing the scaling needed to fit height or fit width, but I am leaving that out here so as to concentrate on the drag support.

import uk.co.senab.photoview.VersionedGestureDetector;
public class ReportImageView extends ImageView implements VersionedGestureDetector.OnGestureListener {

private VersionedGestureDetector mScaleDragDetector;

 public ReportImageView (Context context, AttributeSet attrs){
    super(context, attrs);
    mScaleDragDetector = VersionedGestureDetector.newInstance(context, this);
  }

  @Override
  public void onDrag(float dx, float dy){
     Matrix matrix = getImageMatrix();
     Matrix copy = new Matrix(matrix);
     copy.postTranslate(dx, dy);
     setImageMatrix(copy);
  }
  @Override
  public void onFling(blah, blah...){
    //no op
  }
  @Override
  public void onScale(blah, blah...){
    //no op
  }
}

The salient features are 1) make a new VersionedGestureDetector using the class provided in the PhotoView library, 2) implement the onDrag() method of the OnGestureListener interface. In onDrag() make a new matrix and post-translate it to the coordinates supplied, then set that as the image matrix.

When the scale type is “fit width” the user can drag the image up and down if the height is greater than the width. When the scale type is “fit height” the user can drag the image left or right. If you get such oddball requirements for images, try this solution.

Categories: Development Tags: , ,

Moving An Android View By Dragging It

November 15, 2013 Leave a comment

Yes, here is another article about moving or dragging a view with a finger, but I think I can give a complete example in one place. Most of what I read while developing a movable component did not give a fully working result. I started with the article on making sense of multitouch at the Android developers’ blog. Then I had to go search at Stackoverflow. I give some of those references in the code comments.

I had a requirement to provide a magnifier view, or jeweler’s loupe, which would provide a magnified view of a graph as the user dragged the view over the graph. The magnifier would become visible on a long press and stay visible while the user dragged it over the graph. The frame of the magnifier would display the magnified contents as provided by a helper method (not described here). Here’s a rough example from my testing app.

magnifier example

magnifier example

It shows a small bitmap (unmagnified in this test) and some bogus tooltip values to the right of the image. When this magnifier is dragged over the image (i.e. a real graph), the magnified area will update as will the tooltip information.

Let’s look at the code. Here’s the touch listener for the magnifier. It requires that the magnifier (a RelativeLayout) be passed in on the constructor.

private class TouchListener implements View.OnTouchListener{
   public TouchListener(RelativeLayout frame) {
     super();
     this.frame = frame;
   }
private float aPosX;
private float aPosY;
private float aLastTouchX;
private float aLastTouchY;
private static final int INVALID_POINTER_ID = -1;

// The active pointer is the one currently moving our object.
private int mActivePointerId = INVALID_POINTER_ID;
private RelativeLayout frame =null;

public boolean onTouch(View view, MotionEvent event) {

switch (event.getAction() & MotionEvent.ACTION_MASK) {
   case MotionEvent.ACTION_DOWN:
     //from http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
     Log.d(TAG, "action down");
     // Save the ID of this pointer
     mActivePointerId = event.getPointerId(0);
     final float x = event.getX(mActivePointerId);
     final float y = event.getY(mActivePointerId);
     // Remember where we started
     aLastTouchX = x;
     aLastTouchY = y;
//to prevent an initial jump of the magnifier, aposX and aPosY must
//have the values from the magnifier frame
     if (aPosX == 0){
         aPosX = frame.getX();
      }
      if (aPosY == 0){
          aPosY = frame.getY();
       }
    break;

    case MotionEvent.ACTION_UP:
      Log.d(TAG, "action up");
      reset();
    break;

    case MotionEvent.ACTION_POINTER_DOWN:
    break;

    case MotionEvent.ACTION_POINTER_UP:
      // Extract the index of the pointer that left the touch sensor
       final int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
      final int pointerId = event.getPointerId(pointerIndex);
      if (pointerId == mActivePointerId) {
         // This was our active pointer going up. Choose a new
         // active pointer and adjust accordingly.
         final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
          mActivePointerId = event.getPointerId(newPointerIndex);
       }
  break;
  case MotionEvent.ACTION_MOVE:

     // Find the index of the active pointer and fetch its position
     final int pointerIndexMove = event.findPointerIndex(mActivePointerId);
     Log.d(TAG, "action move");
     float xMove = event.getX(pointerIndexMove);
     float yMove = event.getY(pointerIndexMove);

//from http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
     // Calculate the distance moved
     final float dx = xMove - aLastTouchX;
     final float dy = yMove - aLastTouchY;

     if ( Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop){
        // Move the frame
        aPosX += dx;
        aPosY += dy;

// Remember this touch position for the next move event
//no! see http://stackoverflow.com/questions/17530589/jumping-imageview-while-dragging-getx-and-gety-values-are-jumping?rq=1 and
// last comment in http://stackoverflow.com/questions/16676097/android-getx-gety-interleaves-relative-absolute-coordinates?rq=1
//aLastTouchX = xMove;
//aLastTouchY = yMove;
Log.d(TAG, "we moved");

//in this area would be code for doing something with the magnified view as the frame moves.
       frame.setX(aPosX);
       frame.setY(aPosY);
    }
    break;

    case MotionEvent.ACTION_CANCEL: {
      mActivePointerId = INVALID_POINTER_ID;
    break;
   }
  }

    return true;
}

 private void reset(){
   aPosX = 0;
   aPosY = 0;
   aLastTouchX = 0;
   aLastTouchY = 0;
   frame.setVisibility(View.INVISIBLE);

  }
}

Here is the first important point. At line 29, we see that the magnifier will initially jump from the touch point because the touch event streams relative and absolute coordinates. Prevent this by setting the aPosX and aPosY fields to the initial X and Y coordinates of the frame.

Next, look at line 76 in the case for ACTION_MOVE. The multitouch example from the Android developers’ blog would have us remember the touch position. However that causes problems, as described in the citations from Stackoverflow, so don’t remember the last touch point. If the distance moved is greater than the touchSlop (line 71), just go ahead and move the frame (lines 85 and 86).

With these two modifications to the code shown in the multitouch example you should be able to happily drag a view around to your heart’s content.

Categories: Development Tags: , , ,

A Drawable For A Directional Equilateral Triangle

November 8, 2013 Leave a comment

Recently I needed to provide an up or down pointing triangle to the header of data columns to indicate if the data was sorted in a descending or ascending manner.

triangle drawables

Equilateral triangles indication sort direction

I found this post where the author shows how to create directional equilateral triangle shapes on a canvas. Painting on canvas is a great first step but I needed a Drawable  that could be included into a TextView. I created the EquiTriangleDrawable class (code is below) to use the code for creating triangle shapes on canvas. The class has properties for color and direction.

The class is very simple but very useful. Now I can create a TextView with an optional sort indicator like below. (gridColumn is just a bag of properties.) Since I made the NORTH the default direction, I only have to set direction if the sort order was descending.

EquiTriangleDrawable img = new EquiTriangleDrawable();
img.setBounds( 0, 0, 10, 10 ); img.setColor(textView.getCurrentTextColor());
textView.setCompoundDrawables(null, null, img, null);
textView.setCompoundDrawablePadding(4);
if (gridColumn.sortStatus.equals(Sort.DESCENDING)){    img.setDirection(Direction.SOUTH);
}

Here’s what it looks like in action.

sort indicator in column header

Data table showing sort indicator in Country header

Here’s the code for EquiTriangleDrawable.

package whatever;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;

/** This draws an equilateral triangle within the set bounds, i.e. setBounds(). The triangle will point in the
* specified direction. The default direction is NORTH and the default color is black.
*
*/
public class EquiTriangleDrawable extends Drawable {

private int color = Color.BLACK;
private Direction direction = Direction.NORTH;
/**
*
*/
public EquiTriangleDrawable() {
super();
}
/**
*
*/
public EquiTriangleDrawable(int color, Direction direction) {
super();
this.color = color;
this.direction = direction;
}

public enum Direction {
NORTH, SOUTH, EAST, WEST;
}
/* (non-Javadoc)
* @see android.graphics.drawable.Drawable#draw(android.graphics.Canvas)
*/
@Override
public void draw(Canvas canvas) {
Paint p = new Paint();
p.setStyle(Style.FILL);
p.setColor(getColor());
Path path = getEquilateralTriangle();
canvas.drawPath(path, p);

}

/* no-op
* @see android.graphics.drawable.Drawable#setAlpha(int)
*/
@Override
public void setAlpha(int alpha) {
//
}

/* no-op
* @see android.graphics.drawable.Drawable#setColorFilter(android.graphics.ColorFilter)
*/
@Override
public void setColorFilter(ColorFilter cf) {
//
}

/* Returns zero
* @see android.graphics.drawable.Drawable#getOpacity()
*/
@Override
public int getOpacity() {
//
return 0;
}

/* see http://tech.chitgoks.com/2012/07/08/android-draw-equilateral-triangle-shapes-in-canvas/
*
*/
private Path getEquilateralTriangle() {
Point startPoint = null, p2 = null, p3 = null;
Rect bounds = getBounds();
int width = bounds.right - bounds.left;
switch (direction){
case NORTH:
startPoint = new Point(bounds.left, bounds.bottom);
p2 = new Point(startPoint.x + width, startPoint.y);
p3 = new Point(startPoint.x + (width / 2), startPoint.y - width);
break;
case SOUTH:
startPoint = new Point(bounds.left, bounds.top);
p2 = new Point(startPoint.x + width,startPoint.y);
p3 = new Point(startPoint.x + (width / 2), startPoint.y + width);
break;
case EAST:
startPoint = new Point(bounds.left, bounds.top);
p2 = new Point(startPoint.x, startPoint.y + width);
p3 = new Point(startPoint.x - width, startPoint.y + (width / 2));
break;
case WEST:
default:
startPoint = new Point(bounds.right, bounds.top);
p2 = new Point(startPoint.x, startPoint.y + width);
p3 = new Point(startPoint.x + width, startPoint.y + (width / 2));
break;
}

Path path = new Path();
path.moveTo(startPoint.x, startPoint.y);
path.lineTo(p2.x, p2.y);
path.lineTo(p3.x, p3.y);

return path;
}

public int getColor() {
return color;
}

public void setColor(int color) {
this.color = color;
}

public Direction getDirection() {
return direction;
}

public void setDirection(Direction direction) {
this.direction = direction;
}
}


 

Keep Android ImageView in a Constant 4:3 Aspect Ratio

March 23, 2013 Leave a comment

Recently I had a specification for showing thumbnail images in a constant 4:3 aspect ratio regardless of the aspect ratio of whatever image would be displayed. I found a lot of references to manipulating and scaling the image, but not many for scaling the ImageView itself. I did find one good clue from Bob Lee in this Stackoverflow post, so I adapted it.

In a subclass of ImageView you can override onMeasure(), as below, to force a 4:3 or other aspect ratio on the instances. If you are making thumbnails, you’ll also want to scale the bitmaps so they “fit” per your specifications. The specs from my UI designer were pretty specialized so I won’t include the code I used for scaling.

@Override protected void onMeasure(int widthMeasureSpec,
   int heightMeasureSpec) {
//   let the default measuring occur, then force the desired aspect ratio 
//   on the view (not the drawable).
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
//force a 4:3 aspect ratio   		   
int height = Math.round(width * .75f);
setMeasuredDimension(width, height);
}
Categories: Development Tags:

Detect Dismissal of the Soft Keyboard

February 22, 2013 5 comments

When your app has multiple text entry fields that must be checked for validity before the next action happens, it can be important to know that the user dismissed the soft keyboard via the back button. Typically you would want to listen for key and/or touch events from the fields so as to run your validation routine. However, there is not an easy way to get the information about dismissal of the keyboard.

There was a helpful answer posted to Stackoverflow by  Steelight who offered an override of the onKeyPreIme method that you add to your new instance of the EditText class. This is fine if you can do e = new EditText(inflater.getContext()) but not so good if you have to get your EditText instances via e = (EditText) findViewById().

Try creating a subclass of EditText and overriding its onKeyPreIme method. Then you can use this subclass anywhere, especially if you put it in a library project.  If you make the onKeyPreIme method send a key event and your instances of this subclass listen for key events, you can detect that the keyboard was dismissed and do whatever you’d do when any key event is received.

/**
 * This class overrides the onKeyPreIme method to dispatch a key event if the
 * KeyEvent passed to onKeyPreIme has a key code of KeyEvent.KEYCODE_BACK.
 * This allows key event listeners to detect that the soft keyboard was
 * dismissed.
 *
 */
public class ExtendedEditText extends EditText {

	public ExtendedEditText(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);

	}

	public ExtendedEditText(Context context, AttributeSet attrs) {
		super(context, attrs);

	}

	public ExtendedEditText(Context context) {
		super(context);

	}

	@Override
	public boolean onKeyPreIme(int keyCode, KeyEvent event) {
		if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
			dispatchKeyEvent(event);
			return false;
		}
		return super.onKeyPreIme(keyCode, event);
	}

}

Categories: Development Tags:

Creating Groups Or Categories For Your Settings Preferences

February 18, 2013 1 comment

The Android API for creating a list of preference headers does not provide a means of categorizing or grouping those headers. Gouchet wrote about his investigation of the code in Android’s Settings application and offered a header adapter class that supports category headers. His class is based upon the header adapter found in the Settings.java class of that application.

While comparing Gouchet’s adapter with the adapter found in Settings.java, I noticed that Gouchet’s adapter omitted some of the code that supports view recycling. While Gouchet’s article was mainly about providing On/Off switches for a preference, a robust header adapter should handle view recycling. To keep you from having to track down Settings.java and extract the adapter code, I’ll show my version of HeaderAdapter. I did not need headers that provide On/Off switches so I left that part out. If you need such headers, use Gouchet’s download of his example.

private static class PrefsHeaderAdapter extends ArrayAdapter</pre>
<header>{

static final int HEADER_TYPE_CATEGORY = 0;
 static final int HEADER_TYPE_NORMAL = 1;

private static class HeaderViewHolder {
 ImageView icon;
 TextView title;
 TextView summary;
 }

private LayoutInflater mInflater;

public PrefsHeaderAdapter(Context context, List
<header>objects) {
 super(context, 0, objects);

mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

@Override
 public View getView(int position, View convertView, ViewGroup parent) {
 HeaderViewHolder holder;
 Header header = getItem(position);
 int headerType = getHeaderType(header);
 View view = null;
 if (convertView == null) {
 holder = new HeaderViewHolder();

 switch (headerType) {
 case HEADER_TYPE_CATEGORY:
 view = mInflater.inflate(android.R.layout.preference_category, parent, false);
 holder.title = ((TextView) view.findViewById(android.R.id.title));
 holder.title.setText(header.getTitle(getContext().getResources()));
 view.setTag(holder);
 break;

case HEADER_TYPE_NORMAL:
 view = mInflater.inflate(R.layout.preference_header_item, parent, false);
 ImageView image = ((ImageView) view.findViewById(android.R.id.icon));
 holder.icon = image;
 image.setImageResource(header.iconRes);
 holder.title = ((TextView) view.findViewById(android.R.id.title));
 holder.title.setText(header.getTitle(getContext().getResources()));
 holder.summary = ((TextView) view.findViewById(android.R.id.summary));
 holder.summary.setText(header.getSummary(getContext().getResources()));
 view.setTag(holder);
 break;

default:
 break;
 }
 } else {
 view = convertView;
 holder = (HeaderViewHolder) view.getTag();
 }

// All view fields must be updated every time, because the view may
 // be recycled
switch (headerType) {
 case HEADER_TYPE_CATEGORY:
 holder.title.setText(header.getTitle(getContext().getResources()));
 break;
 case HEADER_TYPE_NORMAL:
 if (null != holder.icon) {
 holder.icon.setImageResource(header.iconRes);
 }
 holder.title.setText(header.getTitle(getContext().getResources()));
 CharSequence summary = header.getSummary(getContext().getResources());
 if (null != summary) {
 if (!TextUtils.isEmpty(summary)) {
 holder.summary.setVisibility(View.VISIBLE);
 holder.summary.setText(summary);
 } else {
 holder.summary.setVisibility(View.GONE);
 }
 }
 break;
 default:
 }
 return view;
 }

 public static int getHeaderType(Header header) {
 if ((header.fragment == null) && (header.intent == null)) {
 return HEADER_TYPE_CATEGORY;
 }
 return HEADER_TYPE_NORMAL;

 }
}

Using this adapter allowed me to create a grouping header labeled “CONNECTIONS” above a list of server connections in the Settings area of our app (see below for portion of the screen).

The CONNECTIONS category groups the server accounts

The CONNECTIONS category groups the server accounts

Note that if your settings allows users to add or remove items, such as accounts, then view recycling can result in an unexpected type of view holder. You might get a normal type from “convertView” but the header type is category for that position. The code in Settings.java manipulates index counters when an account is added. I added an int field to HeaderViewHolder  and set it to the header type. Then if the holder retrieved from the view tag is not the same type as the header, I null it out and create a new one.

Categories: Development Tags: ,

The Android Vertical Line Problem

December 6, 2012 1 comment

Getting a thin vertical line to use as a divider or other indicator is surprisingly difficult. Various solutions to this problem have been offered at Stackoverflow, e.g. “Android vertical line XML“. Recently I needed a vertical line to use as a divider between items in an open source tooltip component. The default divider was provided as a narrow Textview with a gray background and no text, which was contained within a RelativeLayout. That’s a rather heavyweight implementation, and the divider did not extend the full height of the tooltip, either.

I tried several of the approaches outlined in the Stackoverflow post referenced above and elsewhere. These included a rotated shape drawable and a view with a layout width of 2 pixels, but these did not work. Usually I got a result like below, where the gray divider view caused the tooltip to expand in size and the divider crowded the remaining items out of the tooltip. (An item is made up of text and an icon, like the “Next” and down arrow, although each is optional. See below.)

tooltip_wrong I then adapted a technique we’d used before to get a divider appearing in TextViews used as table cells. We subclassed the TextView and overrode the onDraw method. We drew a narrow vertical line at the left edge to make a nice divider between adjacent table cells. Here’s the (somewhat condensed) code:

private Paint paint = new Paint();
private void init(Context context) {
	paint.setColor(context.getResources().getColor(
			R.color.cell_divider_line));
	paint.setStrokeWidth(0f);
	paint.setStyle(Paint.Style.FILL);
}
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	canvas.drawLine(0, 0, 0, getHeight(), paint);
}

This tooltip component supports both horizontal and vertical orientations so dividers are needed for both cases. The original views for each orientation had a RelativeLayout as the outer view that contains a single tooltip item (an icon and text), so I subclassed RelativeLayout, as below (condensed). This RelativeLayoutWithDivider class provides for a right divider, a bottom divider or no divider.

public enum DividerOrientation  {RIGHT, BOTTOM, NONE}
private Paint paint ;
private Context context;
private DividerOrientation orientation = DividerOrientation.NONE;

private void init() {
	paint = new Paint();
	paint.setColor(context.getResources().getColor(
		R.color.cell_divider_line));
	paint.setStrokeWidth(3f);
	paint.setStyle(Paint.Style.FILL);
}

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	switch (orientation) {
           case RIGHT:
         	init();
            	canvas.drawLine(getWidth(), 0, getWidth(), getHeight(), paint);
                break;
         case BOTTOM:
        	init();
           	canvas.drawLine(0, getHeight(), getWidth(), getHeight(), paint);
         	break;
           default:
         //no divider line
              break;
	}
}

public void setDividerOrientation(DividerOrientation orientation) {
	this.orientation = orientation;
}

vertical tooltip
Logic in the method the populates the items in the tooltip determines which enum to pass on a call to setDividerOrientation such that single items don’t show a divider nor does the last item. (That method is found in the QuickAction class of the open-source project.)

tooltip4cropped
This will make more sense when I show the layout XML for a horizontal tooltip item. Here the parent view is our subclassed RelativeLayout. Nothing had to change in this XML save the class name in the outermost tag.


Categories: Development Tags: , ,
Follow

Get every new post delivered to your Inbox.