Кастомный View с плавным двусторонним скроллингом и эффектом флинга как в WebVew

Flippy

Всем привет! Пишу кастомный View, наследованный от RelativeLayout, в него динамически добавляются View элементы. При его создании я устанавливаю минимальную ширину и высоту в зависимости от ширины и высоты экрана.

Решил добавить возможность прокрутки, для этого обернул кастомную View в HorizontalScrollView, а его - в ScrollView. Но прокрутки не были одновременными, тоесть скроллить можно было только в одну сторону. Тогда я создал кастомные ScrollView и HorizontalScrollView в которых переопределил onTouchEvent, возвращая false. Тогда onTouchEvent я реализовал в активности. В нем я скроллил HorizontalScrollView и ScrollView одновременно. Но тогда прокрутка не была идеальной. При отпускании пальца скроллинг останавливался. А нужен был эффект прокрутки например как в списке, чтобы при отпускании пальца при быстром скролле он продолжался, но затухал.

Нашел эту статью. Сделал все как по ней, правда долго не понимал что такое скроллер. Итог: упущен день, ничего не скроллится и я решил попросить помощи у профессионалов.

Кастомный View

public class MetroMapView extends RelativeLayout
{
Context ctx;
List<station> list;
LayoutInflater inflater;
DisplayMetrics metrics;
List<view> stations;
Paint line;
public Scroller scroller;
int mapWidth, mapHeight;
VelocityTracker mVelocityTracker;
int oldX = -1, oldY = -1;
int dx = 0, dy = 0;

public MetroMapView(Context ctx, AttributeSet attrs)
{
    super(ctx, attrs);
    this.ctx = ctx;
    scroller = new Scroller(getContext());
    line = new Paint();
    line.setStrokeWidth(10);
    line.setStyle(Paint.Style.FILL);
    line.setColor(Color.WHITE);
    inflater = (LayoutInflater)ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    stations = new ArrayList<>();
    metrics = new DisplayMetrics();
    ((GameActivity)ctx).getWindowManager().getDefaultDisplay().getMetrics(metrics);
    //ширина и высота карты
    mapWidth = metrics.widthPixels * 3;
    mapHeight = metrics.heightPixels * 2;

    //установка размеров
    setMinimumHeight(mapHeight);
    setMinimumWidth(mapWidth);
}

void setElements(ArrayList<station> list)
{
    this.list = list;
    //перебираем все станции
    for(Station s : list)
    {
        //инфлэйт
        View station = inflater.inflate(R.layout.station_view, null);

        //установка id и координат
        station.setId(View.generateViewId());
        station.setX(s.getX());
        station.setY(s.getY());

        //добавляем станцию в RelativeLayout
        addView(station);
    }
}

@Override
protected void onDraw(Canvas canvas)
{
    super.onDraw(canvas);
    if(list != null)
    {
        for(Station s : list)
        {
            if(s.getAnchorX() != 0 && s.getAnchorY() != 0)
            {
                float fromX = s.getX() + 25;
                float fromY = s.getY() + 25;
                float toX = s.getAnchorX() + 25;
                float toY = s.getAnchorY() + 25;
                canvas.drawLine(fromX, fromY, toX, toY, line);
            }
        }
    }
}

@Override
public boolean onTouchEvent(MotionEvent ev)
{
    int action = ev.getAction();
    switch (action)
    {
        case MotionEvent.ACTION_DOWN: {
                mVelocityTracker = VelocityTracker.obtain();
                oldX = (int) ev.getX();
                oldY = (int) ev.getY();
                return true;
            }

        case MotionEvent.ACTION_MOVE: {
                mVelocityTracker.addMovement(ev);
                int deltaX = (int) (oldX - ev.getX());
                int deltaY = (int) (oldY - ev.getY());
                oldX = (int) ev.getX();
                oldY = (int) ev.getY();
                //yScroll.scrollBy(deltaX, deltaY);
                //xScroll.scrollBy(deltaX, deltaY);
                scroller = new Scroller(getContext());
                scroller.startScroll(dx, dy, -deltaX, -deltaY);
                return true;
            }

        case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(ev);
                mVelocityTracker.computeCurrentVelocity(1000);
                int vx = (int) mVelocityTracker.getXVelocity();
                int vy = (int) mVelocityTracker.getYVelocity();
                vx = vx * 3 / 4;
                vy = vy * 3 / 4;
                scroller = new Scroller(getContext());
                scroller.fling(dx, dy, vx, vy, 0, 1500, 0, 1500);
                mVelocityTracker.recycle();
                return true;
            }
    }
    return true;
}
}
</station></view></station>

Разметка

<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content">

<!--<ru.albatros.metropolis.VScroll
    android:id="@+id/yScroller"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content">

    <ru.albatros.metropolis.HScroll
        android:id="@+id/xScroller"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content">
-->
        <ru.albatros.metropolis.metromapview android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#527dad">

    <!--</ru.albatros.metropolis.HScroll>

</ru.albatros.metropolis.VScroll>
-->
</ru.albatros.metropolis.metromapview></linearlayout>

Неужели нет библиотеки?

1 ответ

Flippy

Ну допустим у Вас был код, когда вы оборачивали контентную View в HorizontalScrollView, а его в ScrollView.

Можно попробовать при инициализации HorizontalScrollView и ScrollView, установить им Smooth Scrolling:

mHorisontalScrollView.setSmoothScrollingEnabled(true);
mScrollView.setSmoothScrollingEnabled(true);

Как вариант, Вам лучше бы создать кастомный TouchHostView, унаследованный допустим от FrameLayout, в который вы и поместите весь, уже созданный агрегат :)

Должен получиться TouchHostView, внутри которого ScrollView, внутри которого HorizontalScrollView, внутри которого Ваша кастомная View.

После этого переопределить метод dispatchTouchEvent у TouchHostView следующим образом:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean dispatch = mCustomView.dispatchTouchEvent(ev);
    if (!dispatch) {
        dispatch = mHorisontalScrollView.dispatchTouchEvent(ev);
        dispatch |= mScrollView.dispatchTouchEvent(ev);
    }
    return dispatch;
}

В теории это должно помочь Вам :)

Примерно так должен выглядеть TouchHostView:

public class TouchHostView extends FrameLayout {

    private View mHorizontalScrollView;
    private View mScrollView;
    private View mCustomView;

    public TouchHostView(Context context) {
        super(context);
        inflate();
    }

    public TouchHostView(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflate();
    }

    public TouchHostView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        inflate();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public TouchHostView(Context context, AttributeSet attrs, 
                         int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        inflate();
    }

    private void inflate() {
        LayoutInflater inflater = (LayoutInflater) getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.touch_host, this, true);

        mHorisontalSrollView = findViewById(R.id.h_scroll);
        mScrollView = findViewById(R.id.v_scroll);
        mCustomView = findViewById(R.id.custom);

        ((HorizontalScrollView) mHorizontalScrollView)
                .setSmoothScrollingEnabled(true);
        ((ScrollView) mScrollView).setSmoothScrollingEnabled(true);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean dispatch = mCustomView.dispatchTouchEvent(ev);
        if (!dispatch) {
            dispatch = mHorisontalScrollView.onTouchEvent(ev);
            dispatch |= mScrollView.onTouchEvent(ev);
        }
        return dispatch;
    }
}

А его разметка примерно так:

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <scrollview>

        <horizontalscrollview>

            <customview>

        </customview></horizontalscrollview>

    </scrollview>

</merge>

licensed under cc by-sa 3.0 with attribution.