Android: Enable Scrollbars on Canvas-Based View

2019-01-14 20:05发布

问题:

I have a custom view that extends View. It displays drawn shapes, and allows the user to add to those drawings via drawing touch events to the View via onDraw.

I've enabled the ScaleGestureDetector so that the user can zoom in on a particular section and draw, but as I am using single-touch to draw, they cannot use their finger to pan around the zoomed in View.

I've tried to enable scrollbars for the View such that when they are zoomed in, the scrollbars are displayed and can be used by the user to pan... but I simply can't get the scrollbars to be displayed.

Essentially, what I am doing is to call the View's awakenScrollBars() method in my ScaleListener's onScale() method when the user is zoomed in, which triggers invalidate(). I enabled the scrollbars via both the XML, and programatically in onCreate(), but I can't trigger the scrollbars to be visible. Here is my XML:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.package.name.Canvas
    android:id="@+id/canvas"
    android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:focusable="true"
        android:scrollbars="horizontal|vertical" />
</FrameLayout>

And here is my onCreate():

// set scrollbars
setHorizontalScrollBarEnabled(true);
setVerticalScrollBarEnabled(true);

In onDraw, I can verify that the scrollbars are enabled via isHorizontalScrollBarEnabled() and isVerticalScrollBarEnabled(), and awakenScrollBars() in onScale() returns true, but the scroll bars are just not visible.

Any suggestions on how to proceed? Containing the custom View in a ScrollView layout doesn't seem to be an option, as that only supports vertical scrolling.

Thanks,

Paul

回答1:

If you are creating your custom view programatically, then the answer is the following: in order to show scrollbars on custom view class, the method "initializeScrollbars" should be called during initialization (in constructor for example).

This method takes one very obscure parameter of type TypedArray. To get suitable TypedArray instance you need to create custom styleable entry - just create file "attrs.xml" in your "res\values" directory with the following content:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="View">
    <attr name="android:background"/>
    <attr name="android:clickable"/>
    <attr name="android:contentDescription"/>
    <attr name="android:drawingCacheQuality"/>
    <attr name="android:duplicateParentState"/>
    <attr name="android:fadeScrollbars"/>
    <attr name="android:fadingEdge"/>
    <attr name="android:fadingEdgeLength"/>
    <attr name="android:fitsSystemWindows"/>
    <attr name="android:focusable"/>
    <attr name="android:focusableInTouchMode"/>
    <attr name="android:hapticFeedbackEnabled"/>
    <attr name="android:id"/>
    <attr name="android:isScrollContainer"/>
    <attr name="android:keepScreenOn"/>
    <attr name="android:longClickable"/>
    <attr name="android:minHeight"/>
    <attr name="android:minWidth"/>
    <attr name="android:nextFocusDown"/>
    <attr name="android:nextFocusLeft"/>
    <attr name="android:nextFocusRight"/>
    <attr name="android:nextFocusUp"/>
    <attr name="android:onClick"/>
    <attr name="android:padding"/>
    <attr name="android:paddingBottom"/>
    <attr name="android:paddingLeft"/>
    <attr name="android:paddingRight"/>
    <attr name="android:paddingTop"/>
    <attr name="android:saveEnabled"/>
    <attr name="android:scrollX"/>
    <attr name="android:scrollY"/>
    <attr name="android:scrollbarAlwaysDrawHorizontalTrack"/>
    <attr name="android:scrollbarAlwaysDrawVerticalTrack"/>
    <attr name="android:scrollbarDefaultDelayBeforeFade"/>
    <attr name="android:scrollbarFadeDuration"/>
    <attr name="android:scrollbarSize"/>
    <attr name="android:scrollbarStyle"/>
    <attr name="android:scrollbarThumbHorizontal"/>
    <attr name="android:scrollbarThumbVertical"/>
    <attr name="android:scrollbarTrackHorizontal"/>
    <attr name="android:scrollbarTrackVertical"/>
    <attr name="android:scrollbars"/>
    <attr name="android:soundEffectsEnabled"/>
    <attr name="android:tag"/>
    <attr name="android:visibility"/>
</declare-styleable>
</resources>

Also the complete scrollbars initialization code is (place it on your custom view constructor):

setHorizontalScrollBarEnabled(true);
setVerticalScrollBarEnabled(true);

TypedArray a = context.obtainStyledAttributes(R.styleable.View);
initializeScrollbars(a);
a.recycle();

P.S. the solution was tested on Android 2.0

I have forgot to add: also "computeVerticalScrollRange" and "computeHorizontalScrollRange" methods should be overridden. They should just return imaginary width and height of the canvas.



回答2:

For those of you trying to add scrollbars to a custom ViewGroup (rather than View) or one of its subclasses, make sure you add setWillNotDraw(false); to your constructor in addition to doing what Ruslan Yanchyshyn said in their answer above.

More info can be seen in this question and my answer.



回答3:

As we all known, the Android view has the ability to scroll.

There are two things to do in order to show the scroll bars.

  1. add android:scrollbars="horizontal|vertical" in the xml declaration of your view
  2. override the computeHorizontalScrollRange/ computeVerticalScrollRange method of the View class, making the return values large than the return values of computeHorizontalScrollExtent/computeVerticalScrollExtent method

After that the scrollbars should automatically show when you invoke the scrollTo or scrollBy method.

Sorry for my poor English, if there is any spell or grammar mistakes. you can try this out:
activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="10dp"
tools:context=".MainActivity" >

<com.netease.test.testscroll.ScrollImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFCCCCCC"
    android:scrollbars="horizontal|vertical"
    android:src="@drawable/pp" />

</LinearLayout>

ScrollImageView is a custom view, you can implement like this:

public class ScrollImageView extends ImageView {
    static final String TAG = "ScrollImageView";
    private Rect mContentRect = new Rect();
    GestureDetector mDetector;
    OnGestureListener mListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            Log.i("onScroll", "before:distanceX = " + distanceX
                    + ", distanceY = " + distanceY);
            scrollBy((int)distanceX, (int)distanceY);
            // boolean value = awakenScrollBars();
            Log.i("onScroll", "after:current ScrollX=" + getScrollX()
                    + ", ScrollY=" + getScrollY());
            return true;
        }
    };
    public ScrollImageView(Context context) {
        this(context, null);
    }
    public ScrollImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public ScrollImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setScaleType(ScaleType.MATRIX);
        mDetector = new GestureDetector(getContext(), mListener);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDetector.onTouchEvent(event);
        return true;
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mContentRect.set(getPaddingLeft(), getPaddingTop(), getWidth()
                - getPaddingRight(), getHeight() - getPaddingBottom());
    }
    @Override
    protected int computeHorizontalScrollRange() {
        return getDrawable().getIntrinsicWidth();
    }
    @Override
    protected int computeHorizontalScrollExtent() {
        return mContentRect.width();
    }
    @Override
    protected int computeHorizontalScrollOffset() {
        return Math.max(0, getScrollX());
    }
    private int getScrollRangeX() {
        return computeHorizontalScrollRange() - computeHorizontalScrollExtent();
    }
    @Override
    protected int computeVerticalScrollRange() {
        return getDrawable().getIntrinsicHeight();
    }
    @Override
    protected int computeVerticalScrollExtent() {
        return mContentRect.height();
    }
    @Override
    protected int computeVerticalScrollOffset() {
        return Math.max(0, getScrollY());
    }
    private int getScrollRangeY() {
        return computeVerticalScrollRange() - computeVerticalScrollExtent();
    }
}

specify a large png named "pp", and just scroll the image, you should see the scrollbars.



回答4:

The method "initializeScrollbars" has been removed from API level 21 onwards.

    final TypedArray a = context.obtainStyledAttributes(R.styleable.View);
    initializeScrollbars(a);//removed method in Lollipop.
    a.recycle();

See. https://developer.android.com/sdk/api_diff/21/changes/android.view.View.html. initializeScrollbars is undefined?

FYI. We cannot use the method initializeScrollbars(a) anymore in Lollipop.