Coachmarks are used in Android Apps to provide in-app help by overlaying the GUI with arrows and text to show users what to do. Either images can be used or arrows and lines can be created on the fly. Creating an arrow with a curved line programmatically requires the creation of a custom View.
Create an attribute file in ./res/values/ which is used to indicate the parameters that can be passed from layout files to the view.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CoachmarkArrow">
<attr name="linewidth" format="dimension" />
<attr name="col" format="color" />
<attr name="fromx" format="float"/>
<attr name="fromy" format="float"/>
<attr name="midx" format="float"/>
<attr name="midy" format="float"/>
<attr name="tox" format="float"/>
<attr name="toy" format="float"/>
<attr name="arrow1x" format="float"/>
<attr name="arrow1y" format="float"/>
<attr name="arrow2x" format="float"/>
<attr name="arrow2y" format="float"/>
</declare-styleable>
</resources>
In the layout file which show the Coachmark overlay have the following:
<com.example.CoachmarkArrow
xmlns:coachmark="http://schemas.android.com/apk/res/com.example"
android:id="@+id/coachMenu"
android:layout_height="700dp"
android:layout_width="100dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
coachmark:linewidth="5dp"
coachmark:col="@android:color/white"
coachmark:fromx="1.0"
coachmark:fromy="0.8"
coachmark:midx="0.4"
coachmark:midy="0.7"
coachmark:tox="0.2"
coachmark:toy="0.1"
coachmark:arrow1x="0.1"
coachmark:arrow1y="0.15"
coachmark:arrow2x="0.3"
coachmark:arrow2y="0.15"
/>
The code for the custom view is:
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import com.nelladragon.showertimertalking.R;
import java.security.InvalidParameterException;
public class CoachmarkArrow extends View {
private static final String TAG = CoachmarkArrow.class.getSimpleName();
public static final float NOT_USED = -1;
Paint paint;
PointF fromPoint = null;
PointF midPoint = null;
PointF toPoint = null;
PointF arrow1Point = null;
PointF arrow2Point = null;
public CoachmarkArrow(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CoachmarkArrow, 0, 0);
processPoints(a);
float fromX = a.getFloat(R.styleable.CoachmarkArrow_fromx, NOT_USED);
float fromY = a.getFloat(R.styleable.CoachmarkArrow_fromy, NOT_USED);
float midX = a.getFloat(R.styleable.CoachmarkArrow_midx, NOT_USED);
float midY = a.getFloat(R.styleable.CoachmarkArrow_midy, NOT_USED);
float toX = a.getFloat(R.styleable.CoachmarkArrow_tox, NOT_USED);
float toY = a.getFloat(R.styleable.CoachmarkArrow_toy, NOT_USED);
float arrow1X = a.getFloat(R.styleable.CoachmarkArrow_arrow1x, NOT_USED);
float arrow1Y = a.getFloat(R.styleable.CoachmarkArrow_arrow1y, NOT_USED);
float arrow2X = a.getFloat(R.styleable.CoachmarkArrow_arrow2x, NOT_USED);
float arrow2Y = a.getFloat(R.styleable.CoachmarkArrow_arrow2y, NOT_USED);
if (fromX == NOT_USED || fromY == NOT_USED || toX == NOT_USED || toY == NOT_USED) {
throw new InvalidParameterException("fromx, fromy, tox, toy must be specified");
}
this.fromPoint = new PointF(fromX, fromY);
this.toPoint = new PointF(toX, toY);
if (midX != NOT_USED && midY != NOT_USED) {
this.midPoint = new PointF(midX, midY);
}
if (arrow1X != NOT_USED && arrow1Y != NOT_USED) {
this.arrow1Point = new PointF(arrow1X, arrow1Y);
}
if (arrow2X != NOT_USED && arrow2Y != NOT_USED) {
this.arrow2Point = new PointF(arrow2X, arrow2Y);
}
processPaint(a);
a.recycle();
}
private void processPoints(TypedArray a) {
float fromX = a.getFloat(R.styleable.CoachmarkArrow_fromx, NOT_USED);
float fromY = a.getFloat(R.styleable.CoachmarkArrow_fromy, NOT_USED);
float midX = a.getFloat(R.styleable.CoachmarkArrow_midx, NOT_USED);
float midY = a.getFloat(R.styleable.CoachmarkArrow_midy, NOT_USED);
float toX = a.getFloat(R.styleable.CoachmarkArrow_tox, NOT_USED);
float toY = a.getFloat(R.styleable.CoachmarkArrow_toy, NOT_USED);
float arrow1X = a.getFloat(R.styleable.CoachmarkArrow_tox, NOT_USED);
float arrow1Y = a.getFloat(R.styleable.CoachmarkArrow_toy, NOT_USED);
float arrow2X = a.getFloat(R.styleable.CoachmarkArrow_tox, NOT_USED);
float arrow2Y = a.getFloat(R.styleable.CoachmarkArrow_toy, NOT_USED);
if (fromX == NOT_USED || fromY == NOT_USED || toX == NOT_USED || toY == NOT_USED) {
throw new InvalidParameterException("fromx, fromy, tox, toy must be specified");
}
this.fromPoint = new PointF(fromX, fromY);
this.toPoint = new PointF(toX, toY);
if (midX != NOT_USED && midY != NOT_USED) {
this.midPoint = new PointF(midX, midY);
}
if (arrow1X != NOT_USED && arrow1Y != NOT_USED && arrow2X != NOT_USED && arrow2Y != NOT_USED) {
this.arrow1Point = new PointF(arrow1X, arrow1Y);
this.arrow2Point = new PointF(arrow2X, arrow2Y);
}
}
private void processPaint(TypedArray a) {
paint = new Paint();
paint.setColor(a.getColor(R.styleable.CoachmarkArrow_col, Color.WHITE));
paint.setStrokeWidth(a.getDimensionPixelSize(R.styleable.CoachmarkArrow_linewidth, 0));
paint.setStrokeCap(Paint.Cap.BUTT);
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
int width = canvas.getWidth();
int height = canvas.getHeight();
// If there is a mid-point, draw a spline, otherwise just draw a line.
if (this.midPoint != null) {
float[] x = new float[3];
float[] y = new float[3];
if (fromPoint.x > toPoint.x) {
x[0] = toPoint.x * width;
x[1] = midPoint.x * width;
x[2] = fromPoint.x * width;
y[0] = toPoint.y * height;
y[1] = midPoint.y * height;
y[2] = fromPoint.y * height;
} else {
x[0] = fromPoint.x * width;
x[1] = midPoint.x * width;
x[2] = toPoint.x * width;
y[0] = fromPoint.y * height;
y[1] = midPoint.y * height;
y[2] = toPoint.y * height;
}
int start = (int) x[0];
int end = (int) x[2];
int lastX = -1;
int lastY = -1;
Spline spline = Spline.createSpline(x, y);
for (int aX = start; aX <= end; aX++) {
int aY = (int) spline.interpolate(aX);
if (lastX != -1) {
canvas.drawLine(lastX, lastY, aX, aY, paint);
}
lastX = aX;
lastY = aY;
}
} else {
canvas.drawLine(fromPoint.x * width, fromPoint.y * height, toPoint.x * width, toPoint.y * height, paint);
}
// Draw the arrow if needed.
if (this.arrow1Point != null) {
canvas.drawLine(arrow1Point.x * width, arrow1Point.y * height, toPoint.x * width, toPoint.y * height, paint);
canvas.drawLine(arrow2Point.x * width, arrow2Point.y * height, toPoint.x * width, toPoint.y * height, paint);
}
}
}
The code above uses curve fitting spline code from here: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util/Spline.java
Recent Comments