Подтвердить что ты не робот

Расширение RelativeLayout и переопределение dispatchDraw() для создания масштабируемой ViewGroup

Я видел, как несколько человек спрашивают, как увеличить всю группу ViewGroup (например, RelativeLayout) за один раз. На данный момент это то, что мне нужно достичь. Самый очевидный подход, для меня, заключался бы в том, чтобы удерживать масштабный коэффициент масштабирования как одну переменную где-то; и в каждом из методов дочернего вида onDraw() этот масштабный коэффициент будет применяться к холсту до рисования графики.

Однако, прежде чем делать это, я попытался быть умным (ха - обычно плохая идея) и расширить RelativeLayout, в класс под названием ZoomableRelativeLayout. Моя идея заключается в том, что любое преобразование масштаба может быть применено только один раз к Canvas в переопределенной функции dispatchDraw(), так что совершенно не нужно отдельно применять масштаб в любом из дочерних представлений.

Вот что я сделал в моем ZoomableRelativeLayout. Это просто простое расширение RelativeLayout, при этом dispatchDraw() переопределяется:

protected void dispatchDraw(Canvas canvas){         
    canvas.save(Canvas.MATRIX_SAVE_FLAG);       
    canvas.scale(mScaleFactor, mScaleFactor);
    super.dispatchDraw(canvas);     
    canvas.restore();       
}

mScaleFactor управляется ScaleListener в том же классе.

Это действительно работает. Я могу ущипнуть, чтобы увеличить ZoomableRelativeLayout, и все просмотры, проведенные в правильном масштабировании вместе.

За исключением проблемы. Некоторые из этих дочерних просмотров анимированы, и поэтому я периодически вызываю invalidate() на них. Когда масштаб равен 1, эти дочерние представления, как видно, периодически перерисовываются отлично. Когда масштаб отличается от 1, эти анимированные дочерние представления видны только для обновления в части их области просмотра - или вообще не - в зависимости от масштаба масштабирования.

Мое первоначальное мышление состояло в том, что при вызове отдельного дочернего представления invalidate() он может быть перерисован индивидуально системой, а не передан Canvas из родительского RelativeLayout dispatchDraw(), что означает, что дочерний вид заканчивается вверх, освежая себя без примененной шкалы масштабирования. Но странно, что элементы просмотров ребенка, которые перерисовываются на экране, соответствуют правильной шкале масштабирования. Это почти так, как если бы область, которую система решает фактически обновить в базовом растровом файле, остается немасштабированной - если это имеет смысл. Иначе говоря, если у меня есть один анимированный дочерний вид, и я постепенно увеличиваю все дальше и дальше от начального масштаба 1, и если мы разместим воображаемое поле в области, где это дочернее представление, когда масштаб масштабирования равен 1, то вызовы invalidate() только вызывают обновление графики в этом мнимом поле. Но графика, которую, как видно, обновляет, выполняется в правильном масштабе. Если вы увеличиваете масштаб до тех пор, пока детское представление теперь полностью отодвинуто от того места, где оно было со шкалой 1, тогда никакая часть его вообще не будет обновлена. Я приведу еще один пример: представьте, что мой ребенок - это шар, который оживляет, переключаясь между желтым и красным. Если я увеличусь немного, чтобы мяч двигался вправо и вниз, в определенный момент вы увидите только верхнюю левую четверть шара.

Если я постоянно увеличиваю и уменьшаю масштаб изображения, я вижу, как ребенок просматривает анимацию правильно и полностью. Это происходит потому, что вся ViewGroup перерисовывается.

Надеюсь, это имеет смысл; Я пытался объяснить как можно лучше. Могу ли я проиграть с моей масштабируемой стратегией ViewGroup? Есть ли другой способ?

Спасибо,

Трев

4b9b3361

Ответ 1

Если вы применяете масштабный коэффициент к рисунку своих детей, вам также необходимо применить соответствующий коэффициент масштабирования ко всем другим взаимодействиям с ними - отправка событий касания, аннулирование и т.д.

Таким образом, помимо dispatchDraw(), вам необходимо переопределить и соответствующим образом настроить поведение, по крайней мере, этих других методов. Чтобы позаботиться о недействительности, вам необходимо переопределить этот метод, чтобы соответствующим образом настроить дочерние координаты:

http://developer.android.com/reference/android/view/ViewGroup.html#invalidateChildInParent (int [], android.graphics.Rect)

Если вы хотите, чтобы пользователь мог взаимодействовать с дочерними представлениями, вам также необходимо переопределить это, чтобы соответствующим образом настроить координаты касания, прежде чем они будут отправлены детям:

http://developer.android.com/reference/android/view/ViewGroup.html#dispatchTouchEvent(android.view.MotionEvent)

Также я настоятельно рекомендую вам реализовать все это внутри простого подкласса ViewGroup, у которого есть одно дочернее представление, которым он управляет. Это избавит вас от любой сложности поведения, которую RelativeLayout представляет в своей собственной ViewGroup, упрощая то, что вам нужно для решения и отладки в вашем собственном коде. Поместите RelativeLayout в качестве дочернего элемента вашего специального вида ViewGroup.

Наконец, одно улучшение вашего кода - в dispatchDraw() вы хотите сохранить состояние холста после применения коэффициента масштабирования. Это гарантирует, что ребенок не сможет изменить настроенное вами преобразование.

Ответ 2

Отличный ответ от hackbod напомнил мне, что мне нужно опубликовать решение, к которому я в конце концов пришел. Обратите внимание, что это решение, которое работало для меня для приложения, которое я делал в то время, можно было бы улучшить с помощью предложений hackbod. В частности, мне не нужно было обрабатывать события касания, и, пока я не прочитал сообщение hackbod, мне не приходило в голову, что если бы я это сделал, мне тоже нужно было бы масштабировать их.

Напомним, для моего приложения я должен был получить большую диаграмму (в частности, план этажа здания) с другими маленькими "маркерными" символами, наложенными на нее. Символы фоновой диаграммы и переднего плана рисуются с использованием векторной графики (то есть объектов Path() и Paint(), примененных к Canvas в методе onDraw()). Причиной желания создавать все графики таким образом, в отличие от использования ресурсов растрового изображения, является то, что графика конвертируется во время выполнения с использованием моего SVG-конвертера изображений.

Требовалось, чтобы диаграмма и соответствующие символы маркера были дочерними элементами ViewGroup, и все они могли бы быть увеличены с увеличением.

Много кода выглядит беспорядочным (это была спешка для демонстрации), поэтому вместо того, чтобы просто копировать все это, вместо этого я попытаюсь объяснить, как я это сделал с соответствующими битами кода.

Прежде всего, у меня есть ZoomableRelativeLayout.

public class ZoomableRelativeLayout extends RelativeLayout { ...

Этот класс включает в себя классы-слушатели, которые расширяют ScaleGestureDetector и SimpleGestureListener, так что макет может быть изменен и увеличен. Особый интерес представляет прослушиватель жестов масштаба, который устанавливает переменную масштабного коэффициента, а затем вызывает invalidate() и requestLayout(). В настоящий момент я не уверен, что если invalidate() необходимо, но в любом случае - вот оно:

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

@Override
public boolean onScale(ScaleGestureDetector detector){
    mScaleFactor *= detector.getScaleFactor();
    // Apply limits to the zoom scale factor:
    mScaleFactor = Math.max(0.6f, Math.min(mScaleFactor, 1.5f);
    invalidate();
    requestLayout();
    return true;
    }
}

Следующее, что мне пришлось сделать в моем ZoomableRelativeLayout, было переопределить onLayout(). Для этого мне показалось, что полезно смотреть на попытки других людей на масштабируемый макет, а также мне очень полезно посмотреть исходный исходный код Android для RelativeLayout. Мой переопределенный метод копирует многое из RelativeLayout onLayout(), но с некоторыми изменениями.

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
    int count = getChildCount();
    for(int i=0;i<count;i++){
        View child = getChildAt(i); 
        if(child.getVisibility()!=GONE){
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)child.getLayoutParams();
            child.layout(
                (int)(params.leftMargin * mScaleFactor), 
                (int)(params.topMargin * mScaleFactor), 
                (int)((params.leftMargin + child.getMeasuredWidth()) * mScaleFactor), 
                (int)((params.topMargin + child.getMeasuredHeight()) * mScaleFactor) 
                );
        }
    }
}

Что значимо здесь, когда при вызове 'layout()' для всех детей я применяю коэффициент масштабирования к параметрам макета также для этих детей. Это один шаг к решению проблемы отсечения, а также он правильно устанавливает положение x, y детей относительно друг друга для разных масштабных коэффициентов.

Еще одна важная вещь заключается в том, что я больше не пытаюсь масштабировать Canvas в dispatchDraw(). Вместо этого каждый вид Child масштабирует свой холст после получения коэффициента масштабирования из родительского ZoomableRelativeLayout с помощью метода getter.

Далее, я перейду к тому, что мне нужно было сделать в дочернем окне Views моего ZoomableRelativeLayout. В моем ZoomableRelativeLayout есть только один вид View I, содержащий как детей; это представление для рисования графики SVG, которую я называю SVGView. Конечно, материал SVG здесь не уместен. Здесь его метод onMeasure():

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    float parentScale = ((FloorPlanLayout)getParent()).getScaleFactor();
    int chosenWidth, chosenHeight;
    if( parentScale > 1.0f){
        chosenWidth =  (int) ( parentScale * (float)svgImage.getDocumentWidth() );
        chosenHeight = (int) ( parentScale * (float)svgImage.getDocumentHeight() );
    }
    else{
        chosenWidth =  (int) (  (float)svgImage.getDocumentWidth() );
        chosenHeight = (int) (  (float)svgImage.getDocumentHeight() );          
    }

    setMeasuredDimension(chosenWidth, chosenHeight);
}

И onDraw():

@Override
protected void onDraw(Canvas canvas){   
    canvas.save(Canvas.MATRIX_SAVE_FLAG);       

    canvas.scale(((FloorPlanLayout)getParent()).getScaleFactor(), 
            ((FloorPlanLayout)getParent()).getScaleFactor());       


    if( null==bm  ||  bm.isRecycled() ){
        bm = Bitmap.createBitmap(
                getMeasuredWidth(),
                getMeasuredHeight(),
                Bitmap.Config.ARGB_8888);


        ... Canvas draw operations go here ...
    }       

    Paint drawPaint = new Paint();
    drawPaint.setAntiAlias(true);
    drawPaint.setFilterBitmap(true);

    // Check again that bm isn't null, because sometimes we seem to get
    // android.graphics.Canvas.throwIfRecycled exception sometimes even though bitmap should
    // have been drawn above. I'm guessing at the moment that this *might* happen when zooming into
    // the house layout quite far and the system decides not to draw anything to the bitmap because
    // a particular child View is out of viewing / clipping bounds - not sure. 
    if( bm != null){
        canvas.drawBitmap(bm, 0f, 0f, drawPaint );
    }

    canvas.restore();

}

Снова - как отказ от ответственности, вероятно, есть некоторые бородавки в том, что я там разместил, и мне еще предстоит тщательно изучить предложения hackbod и включить их. Я намерен вернуться и отредактировать это дальше. Тем временем я надеюсь, что он может начать давать полезные указания другим о том, как реализовать масштабируемую ViewGroup.