В этом уроке:
- используем PathMeasure для работы c Path
Объект Path мы подробно изучили в Уроке 143. Сейчас рассмотрим PathMeasure, весьма полезный в некоторых случаях инструмент, который умеет:
- вычислять длину сегментов Path
- определять, закрыт или открыт сегмент
- получать координаты и угол наклона для указанной точки Path
- выделять часть Path в отдельный объект
Создадим проект:
Project name: P1501_PathMeasure
Build Target: Android 2.3.3
Application name: PathMeasure
Package name: ru.startandroid.develop.p1501pathmeasure
Create Activity: MainActivity
MainActivity.java:
package ru.startandroid.develop.p1501pathmeasure;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
public class MainActivity extends Activity {
final String TAG = "myLogs";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(new DrawView(this));
}
class DrawView extends View {
Paint paint;
Paint paintText;
Path path;
PathMeasure pMeasure;
float length;
public DrawView(Context context) {
super(context);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
paintText = new Paint(Paint.ANTI_ALIAS_FLAG);
paintText.setTextSize(30);
path = new Path();
path.moveTo(100, 300);
path.rLineTo(150, 100);
path.rLineTo(150, -100);
path.rQuadTo(150, 200, 300, 0);
path.rLineTo(150, 100);
path.rLineTo(150, -100);
pMeasure = new PathMeasure(path, false);
length = pMeasure.getLength();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawARGB(80, 102, 204, 255);
canvas.drawPath(path, paint);
canvas.drawText(String.format("Length: %s", length), 100, 100,
paintText);
}
}
}
В onCreate мы флагами убираем заголовок окна и переводим приложение в полноэкранный режим. Позже я поясню, зачем это нужно.
В конструкторе DrawView создаем Path, состоящий из нескольких линий и одной кривой. Далее создаем для него PathMeasure, флаг forceClosed при этом ставим false – нам не нужно закрывать Path. Методом getLength получаем длину Path.
В onDraw рисуем Path и выводим на экран его длину.

Теперь попробуем получить геометрическую инфу о произвольной точке Path.
Перепишем DrawView:
class DrawView extends View {
Paint paint;
Paint paintText;
Path path;
PathMeasure pMeasure;
Matrix matrix;
Rect rect;
float[] pos;
float[] tan;
float length;
float distance;
public DrawView(Context context) {
super(context);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
paintText = new Paint(Paint.ANTI_ALIAS_FLAG);
paintText.setTextSize(30);
path = new Path();
path.moveTo(100, 300);
path.rLineTo(150, 100);
path.rLineTo(150, -100);
path.rQuadTo(150, 200, 300, 0);
path.rLineTo(150, 100);
path.rLineTo(150, -100);
pMeasure = new PathMeasure(path, false);
length = pMeasure.getLength();
distance = length / 4;
matrix = new Matrix();
pMeasure.getMatrix(distance, matrix,
PathMeasure.POSITION_MATRIX_FLAG
+ PathMeasure.TANGENT_MATRIX_FLAG);
pos = new float[2];
tan = new float[2];
pMeasure.getPosTan(distance, pos, tan);
rect = new Rect(-20, -10, 20, 10);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawARGB(80, 102, 204, 255);
canvas.drawPath(path, paint);
canvas.drawText(
String.format("Distance: %s of %s", distance, length), 100,
100, paintText);
canvas.drawText(
String.format("Position: %s. Tangent (cos,sin): %s",
Arrays.toString(pos), Arrays.toString(tan)), 100,
150, paintText);
canvas.setMatrix(matrix);
canvas.drawRect(rect, paint);
}
}
В конструкторе DrawView создаем тот же Path, далее создаем для него PathMeasure, измеряем длину и в переменную distance поместим значение равное четверти длины. Далее используем метод getMatrix, передаем в него:
- расстояние от начала Path до точки, информация о которой нам необходима
- матрицу, которая будет заполнена значениями, актуальными для указанной точки
- флаги. Их два POSITION_MATRIX_FLAG – в матрицу попадут данные только по позиции точки, TANGENT_MATRIX_FLAG – в матрицу попадут данные только по повороту в точке. Мы используем сразу оба флага.
Тем самым мы получим матрицу, которая описывает положение и поворот объекта, который находится в точке на расстоянии distance от начала.
Метод getPosTan имеет схожий смысл, но он заполнит не матрицу, а два массива: pos – позиция, tan – наклон (cos и sin угла).
В методе onDraw рисуем Path, выводим значение distance и информацию, полученную из метода getPosTan.
Далее применяем к канве матрицу, полученную из метода getMatrix и рисуем небольшой прямоугольник. Он разместится в точке, которая находится на расстоянии distance и его угол будет соответствовать углу наклона Path в этой точке.

Если из MainActivity.onCreate убрать флаги заголовка и полноэкранного режима, то полученная матрица будет содержать некорректные значения смещения, не учитывающие высоту заголовка и верхней панели. Не знаю, баг это или фича.
Path может состоять из нескольких контуров и PathMeasure умеет их различать.
Перепишем DrawView:
class DrawView extends View {
Paint paint;
Path path;
PathMeasure pMeasure;
public DrawView(Context context) {
super(context);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
path = new Path();
path.moveTo(100, 300);
path.rLineTo(150, 150);
path.rLineTo(150, -100);
path.rMoveTo(0, 0);
path.rQuadTo(150, 200, 300, 0);
path.close();
path.rMoveTo(0, 0);
path.rLineTo(150, 100);
path.rLineTo(150, -150);
path.close();
pMeasure = new PathMeasure(path, false);
do {
Log.d(TAG,
String.format("Length: %s, isClosed: %s",
pMeasure.getLength(), pMeasure.isClosed()));
} while (pMeasure.nextContour());
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawARGB(80, 102, 204, 255);
canvas.drawPath(path, paint);
}
}
При создании Path мы используем методы moveTo и rMoveTo. Эти методы начинают новый контур в Path. Таким образом у нас получилось три контура. Методом close мы закроем второй и третий контур, первый оставим открытым.
Далее используем nextContour, чтобы перебирать контуры и методами getLength и isClosed получаем длину и выясняем закрыт ли контур. Выводим в лог всю эту информацию.

На экране видно, что второй и третий контуры закрыты.
Логи говорят об этом же и показывают длину каждого контура (с учетом закрывающей линии):
Length: 392.4096, isClosed: false
Length: 673.3855, isClosed: true
Length: 696.5477, isClosed: true
Выделим часть одного Path в другой Path.
Перепишем DrawView:
class DrawView extends View {
Paint paint;
Path path;
Path path1;
PathMeasure pMeasure;
public DrawView(Context context) {
super(context);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
path = new Path();
path.moveTo(100, 300);
path.rLineTo(150, 150);
path.rLineTo(150, -100);
path.rQuadTo(150, 200, 300, 0);
path.rLineTo(150, 100);
path.rLineTo(150, -150);
pMeasure = new PathMeasure(path, false);
path1 = new Path();
pMeasure.getSegment(150, 850, path1, true);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawARGB(80, 102, 204, 255);
canvas.drawPath(path1, paint);
}
}
Используем метод getSegment. Передаем туда расстояние до точки начала (150) и до точки конца (850) нужной нам части Path. Объект path1 будет содержать вырезанную часть объекта path. Четвертым параметром передаем true, чтобы полученная фигура начиналась от стартовой точки. Иначе она начнется с (0,0).

Мы получили часть Path между точками, лежащими на расстоянии 150 и 850 от начала фигуры.
На следующем уроке:
- рассматриваем PathEffect-объекты
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

