Расширение RelativeLayout и переопределение dispatchDraw () для создания масштабируемой ViewGroup - PullRequest
24 голосов
/ 17 июня 2011

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

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

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

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

Спасибо,

Трев

Ответы [ 2 ]

14 голосов
/ 28 июня 2011

Отличный ответ от 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. Мой переопределенный метод копирует большую часть того, что есть в onLayout () RelativeLayout, но с некоторыми изменениями.

@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 (). Вместо этого каждый дочерний View масштабирует свой Canvas после получения коэффициента масштабирования из родительского ZoomableRelativeLayout с помощью метода get.

Далее я перейду к тому, что я должен был сделать в дочерних представлениях моего ZoomableRelativeLayout. В моем ZoomableRelativeLayout есть только один тип View, который я использую как дочерний элемент; это представление для рисования графики 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.

14 голосов
/ 27 июня 2011

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

Таким образом, в дополнение к 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 () вы хотите сохранить состояние холста после применения коэффициента масштабирования. Это гарантирует, что ребенок не сможет изменить преобразование, которое вы установили.

...