This question's almost a year old, but it's the top in a google search for this problem, so I thought I'd add some details.
If you're looking for a generic one direction view that can scroll, the other answers of using Android's built in ScrollView will work fine.
However, if you want the multidirectional aspect of iOS's UIScrollView, you'll have to design/use a custom view (or use a hack such as using a webview). An example implementation of a view that can do this can be found in a related question here:
View with horizontal and vertical pan/drag and pinch-zoom
Then depending on your tastes, you can modify it as needed, such as this implementation without the zooming (just the panning):
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.*;
public class MultiDirectionPanGroup extends ViewGroup {
private static final int INVALID_POINTER_ID = 1;
private int mActivePointerId = INVALID_POINTER_ID;
private float mPosX;
private float mPosY;
private Matrix mTranslateMatrix = new Matrix();
private Matrix mTranslateMatrixInverse = new Matrix();
private float mLastTouchX;
private float mLastTouchY;
private float mFocusY;
private float mFocusX;
private float[] mInvalidateWorkingArray = new float[6];
private float[] mDispatchTouchEventWorkingArray = new float[2];
private float[] mOnTouchEventWorkingArray = new float[2];
public MultiDirectionPanGroup(Context context) {
super(context);
mTranslateMatrix.setTranslate(0, 0);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
child.layout(l, t, l+child.getMeasuredWidth(), t + child.getMeasuredHeight());
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.translate(mPosX, mPosY);
super.dispatchDraw(canvas);
canvas.restore();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
mDispatchTouchEventWorkingArray[0] = ev.getX();
mDispatchTouchEventWorkingArray[1] = ev.getY();
mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
ev.setLocation(mDispatchTouchEventWorkingArray[0],
mDispatchTouchEventWorkingArray[1]);
return super.dispatchTouchEvent(ev);
}
/**
* Although the docs say that you shouldn't override this, I decided to do
* so because it offers me an easy way to change the invalidated area to my
* likening.
*/
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
mInvalidateWorkingArray[0] = dirty.left;
mInvalidateWorkingArray[1] = dirty.top;
mInvalidateWorkingArray[2] = dirty.right;
mInvalidateWorkingArray[3] = dirty.bottom;
mInvalidateWorkingArray = scaledPointsToScreenPoints(mInvalidateWorkingArray);
dirty.set(Math.round(mInvalidateWorkingArray[0]), Math.round(mInvalidateWorkingArray[1]),
Math.round(mInvalidateWorkingArray[2]), Math.round(mInvalidateWorkingArray[3]));
location[0] *= mScaleFactor;
location[1] *= mScaleFactor;
return super.invalidateChildInParent(location, dirty);
}
private float[] scaledPointsToScreenPoints(float[] a) {
mTranslateMatrix.mapPoints(a);
return a;
}
private float[] screenPointsToScaledPoints(float[] a){
mTranslateMatrixInverse.mapPoints(a);
return a;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mOnTouchEventWorkingArray[0] = ev.getX();
mOnTouchEventWorkingArray[1] = ev.getY();
mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);
ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
mLastTouchX = x;
mLastTouchY = y;
// Save the ID of this pointer
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mPosX += dx;
mPosY += dy;
mTranslateMatrix.preTranslate(dx, dy);
mTranslateMatrix.invert(mTranslateMatrixInverse);
mLastTouchX = x;
mLastTouchY = y;
invalidate();
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
// Extract the index of the pointer that left the touch sensor
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.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;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}
}