Custom drawing line between two views does not work as expected
I am trying to draw lines between buttons in android. I have created one custom class that draws a line between buttons inside a Relative Layout (parent layout).
Here is my MatchTheColoumnDrawView.java class, which takes a context, startView, endView, lineColour, endCircleColour, thickness (in float), direction (LEFT_TO_RIGHT or RIGHT_TO_LEFT).
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.Log;
import android.view.View;
/**
* Created by Abhishek on 12/21/2016.
*
* Changes made as per new design on Date 9 Oct 2017
*
*/
public class MatchTheColumnDrawView extends View {
public static final int LEFT_TO_RIGHT = 1, RIGHT_TO_LEFT = 2;
private Paint mLinePaint, mCirclePaint;
private View startView, endView;
private int direction;
private Canvas canvas;
private float dashWidth = 15f;
private float dashGap = 8f;
float[] intervals = new float[]{dashWidth, dashGap};
float phase = 0;
private int LINE_COLOR = Color.parseColor("#BEBEBE");
private int END_CIRCLE_COLOR = Color.parseColor("#FF99CC00");
/**
*
* parametrised constructor draws line from
* @startView
* to
* @endView
* as per
* @direction
* and
* @lineColor
* and
* @endCircleColor
*
* when
* @lineColor == null
* default lineColor is gray
*
* when
* @endCircleColor == null
* default endCircleColor is green
* */
public MatchTheColumnDrawView(Context context,
View startView, View endView,
String lineColor, String endCircleColor,
float thickness, int direction) {
super(context);
mLinePaint = new Paint();
if (lineColor != null) LINE_COLOR = Color.parseColor(lineColor);
mLinePaint.setColor(LINE_COLOR);
mLinePaint.setStrokeWidth(thickness);
mLinePaint.setStyle(Paint.Style.STROKE);
mCirclePaint = new Paint();
if (endCircleColor != null) END_CIRCLE_COLOR = Color.parseColor(endCircleColor);
mCirclePaint.setColor(END_CIRCLE_COLOR);
mCirclePaint.setStrokeWidth(thickness);
this.startView = startView;
this.endView = endView;
this.direction = direction;
//setBackgroundColor To Transparent
super.setBackgroundColor(Color.TRANSPARENT);
}
public View getStartView() {
return startView;
}
public void setStartView(View startView) {
this.startView = startView;
}
public View getEndView() {
return endView;
}
public void setEndView(View endView) {
this.endView = endView;
}
public Canvas getCanvas() {
return canvas;
}
public int getDirection() {
return direction;
}
public void setDirection(int direction) {
this.direction = direction;
}
@Override
protected void onDraw(Canvas canvas) {
this.canvas = canvas;
Log.d("Direction", String.valueOf(direction));
Log.d("Start View Y:", String.valueOf(startView.getY()));
Log.d("Start View H:", String.valueOf(startView.getHeight()));
Log.d("End View Y:", String.valueOf(endView.getY()));
Log.d("End View H:", String.valueOf(endView.getHeight()));
//By default takes LEFT_TO_RIGHT
if (direction == RIGHT_TO_LEFT) {
//For RIGHT TO LEFT
//Calculating Left X And Mid Of Height Y
/*
* ______________
* | |
* This Point ==>> .| |
* | |
* |______________|
* */
float startViewLeftX = startView.getX();
float startViewMidHeightY = startView.getY() + startView.getHeight() / 2;
//Calculating Right X And Mid Of Height Y
/*
* ______________
* | |
* | |. <<== This Point
* | |
* |______________|
* */
float endViewRightX = endView.getX() + endView.getWidth(); //20 is just to remove unwanted padding on Right Side
float endViewMidHeightY = endView.getY() + endView.getHeight() / 2;
Path mPath = new Path();
mPath.moveTo(startViewLeftX, startViewMidHeightY);
mPath.lineTo(endViewRightX, endViewMidHeightY);
DashPathEffect dashPathEffect = new DashPathEffect(intervals, phase);
mLinePaint.setPathEffect(dashPathEffect);
canvas.drawPath(mPath, mLinePaint);
//canvas.drawLine(startViewLeftX, startViewMidHeightY, endViewRightX, endViewMidHeightY, mLinePaint);
canvas.drawCircle(startViewLeftX, startViewMidHeightY, 5, mCirclePaint);
canvas.drawCircle(endViewRightX, endViewMidHeightY, 5, mCirclePaint);
} else {
//FOR LEFT_TO_RIGHT
//Calculating Right X And Mid Of Height Y
/*
* ______________
* | |
* | |. <<== This Point
* | |
* |______________|
* */
float startViewRightX = startView.getX() + startView.getWidth(); //20 is just to remove unwanted padding on Right Side
float startViewMidHeightY = startView.getY() + startView.getHeight() / 2;
//Calculating Left X And Mid Of Height Y
/*
* ______________
* | |
* This Point ==>> .| |
* | |
* |______________|
* */
float endViewLeftX = endView.getX();
float endViewMidHeightY = endView.getY() + endView.getHeight() / 2;
Path mPath = new Path();
mPath.moveTo(startViewRightX, startViewMidHeightY);
mPath.lineTo(endViewLeftX, endViewMidHeightY);
DashPathEffect dashPathEffect = new DashPathEffect(intervals, phase);
mLinePaint.setPathEffect(dashPathEffect);
canvas.drawPath(mPath, mLinePaint);
//canvas.drawLine(startViewRightX, startViewMidHeightY, endViewLeftX, endViewMidHeightY, mLinePaint);
canvas.drawCircle(startViewRightX, startViewMidHeightY, 5, mCirclePaint);
canvas.drawCircle(endViewLeftX, endViewMidHeightY, 5, mCirclePaint);
}
}
@Override
public void setBackgroundColor(int color) {
super.setBackgroundColor(color);
}
}
I wrote one MatchTheFollowingAttempted class that extends the Relative layout and contains logic for drawing buttons and lines between them. Here is the MatchTheFollowingAttempted.java class.
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.RelativeLayout;
import java.util.ArrayList;
/**
* Created by Abhishek on 24-10-2017.
*/
public class MatchTheFollowingAttempted extends RelativeLayout {
private Context mContext;
int numberOfOneSideButtons = 5;
public MatchTheFollowingAttempted(Context context) {
super(context);
mContext = context;
initialiseView();
}
public MatchTheFollowingAttempted(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
initialiseView();
}
public MatchTheFollowingAttempted(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initialiseView();
}
public MatchTheFollowingAttempted(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mContext = context;
initialiseView();
}
public void initialiseView() {
ArrayList<Button> leftSideButtons = new ArrayList<>();
ArrayList<Button> rightSideButtons = new ArrayList<>();
ArrayList<MatchTheColumnDrawView> matchTheColumnDrawViewArrayList = new ArrayList<>();
for (int i = 0; i < numberOfOneSideButtons; i++) {
Button mButton = new Button(mContext);
mButton.setId(View.generateViewId());
RelativeLayout.LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (i != 0) {
layoutParams.addRule(BELOW, leftSideButtons.get(i-1).getId());
}
layoutParams.setMargins(10, 10, 10, 10);
mButton.setLayoutParams(layoutParams);
leftSideButtons.add(mButton);
addView(mButton);
}
for (int i = 0; i < numberOfOneSideButtons; i++) {
Button mButton = new Button(mContext);
mButton.setId(View.generateViewId());
RelativeLayout.LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.addRule(ALIGN_PARENT_RIGHT);
if (i != 0) {
layoutParams.addRule(BELOW, rightSideButtons.get(i-1).getId());
}
layoutParams.setMargins(10, 10, 10, 10);
mButton.setLayoutParams(layoutParams);
rightSideButtons.add(mButton);
addView(mButton);
}
for (int i = 0; i < numberOfOneSideButtons; i++) {
MatchTheColumnDrawView matchTheColumnDrawView = new MatchTheColumnDrawView(mContext, leftSideButtons.get(i), rightSideButtons.get(4-i), null, null, 2.0f, MatchTheColumnDrawView.LEFT_TO_RIGHT);
matchTheColumnDrawViewArrayList.add(matchTheColumnDrawView);
addView(matchTheColumnDrawView);
}
}
}
When I directly use MatchTheFollowingAttempted inside a LinearLayout (Inside Scrollview) it will render the view correctly with buttons and lines in between. As shown in the picture (Image_One). Here is the xml for it.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<MatchTheFollowingAttempted
android:layout_width="match_parent"
android:layout_height="wrap_content">
</MatchTheFollowingAttempted>
</LinearLayout>
</ScrollView>
</RelativeLayout>
But when I add the second MatchTheFollowingAttempted in the linear layout, then the rows will not show (as in the attached Image_Two). Here is the xml with two MatchTheFollowingAttempted in a linear layout.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<MatchTheFollowingAttempted
android:layout_width="match_parent"
android:layout_height="wrap_content">
</MatchTheFollowingAttempted>
<MatchTheFollowingAttempted
android:layout_width="match_parent"
android:layout_height="wrap_content">
</MatchTheFollowingAttempted>
</LinearLayout>
</ScrollView>
</RelativeLayout>
When I run it it won't show the second layout.
source to share
Currently you are adding MatchTheColumnDrawView
programmatically to MatchTheFollowingAttempted
without installation LayoutParams
. There are default values for LayoutParams
, so with only one instance, MatchTheFollowingAttempted
you're just in luck.
So, to fix the problem, you should set LayoutParams
to MatchTheColumnDrawView
s, for example like this:
for (int i = 0; i < numberOfOneSideButtons; i++) {
MatchTheColumnDrawView matchTheColumnDrawView = new MatchTheColumnDrawView(
mContext, leftSideButtons.get(i),
rightSideButtons.get(numberOfOneSideButtons - 1 - i),
null, null,
2.0f, MatchTheColumnDrawView.LEFT_TO_RIGHT);
RelativeLayout.LayoutParams layoutParams = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.addRule(ALIGN_TOP,
leftSideButtons.get(0).getId());
layoutParams.addRule(ALIGN_BOTTOM,
leftSideButtons.get(numberOfOneSideButtons - 1).getId());
matchTheColumnDrawView.setLayoutParams(layoutParams);
matchTheColumnDrawViewArrayList.add(matchTheColumnDrawView);
addView(matchTheColumnDrawView);
}
If your left and right sides Button
have very different heights, you will need to find a better formula, but the idea is to ask for as much space as possible, but not the whole screen (otherwise the next custom View
will not show at all), so try aligning the bottom MatchTheColumnDrawView
to the bottom of the bottom Button
.
PS How did I know?
Using the Inspector Studio Studio Layout on yours View
(thanks BTW for posting the complete code!) Showed that the "missing" View
one actually had a height of 0.
The redefinition onMeasure()
in MatchTheColumnDrawView
to register the measured width and height confirmed this: it RelativeLayout
performs two passes of the layout: with a height of 0 after the first pass, but after the second pass it View
must have some height> 0 or it will not be shown.
EDIT:
In MatchTheFollowingAttempted
you set the padding to each of the buttons Green circles will be drawn exactly in the middle of the edge View
if you subtract the padding when calculating the average height values, for example.
float endViewMidHeightY = endView.getY() + endView.getHeight() / 2 - 10;
Since a Button
has its own padding (it looks smaller than it actually is), the circles still don't overlap the visible edge Button
. To achieve this, either use your own Button background, or (kind of hacky, because padding may change in future Android versions), subtract padding from the x coordinate (left) by adding padding to the x coordinate (right side) accordingly.
The screenshot below shows an example with some custom s View
and some Button
s:
source to share
Hi try the following code
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="50dp">
<Button
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<View
android:layout_weight="5"
android:layout_width="match_parent"
android:background="#313131"
android:layout_gravity="center_vertical"
android:layout_height="3dp"/>
<Button
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" /></LinearLayout>
source to share