diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/CircularFlow.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/CircularFlow.java
new file mode 100644
index 000000000..c24b491f3
--- /dev/null
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/CircularFlow.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.constraintlayout.helper.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
+import androidx.constraintlayout.widget.R;
+import androidx.constraintlayout.widget.VirtualLayout;
+import java.util.Arrays;
+
+/**
+ *
+ * CircularFlow virtual layout.
+ *
+ * Allows positioning of referenced widgets circular.
+ *
+ * The elements referenced are indicated via constraint_referenced_ids, as with other ContraintHelper implementations.
+ *
+ * XML attributes that are needed:
+ *
+ * - constraint_referenced_ids = "view2, view3, view4,view5,view6"
. It receives id's of the views that will add the references.
+ * - circularflow_viewCenter = "view1"
. It receives the id of the view of the center where the views received in constraint_referenced_ids will be referenced.
+ * - circularflow_angles = "45,90,135,180,225"
. Receive the angles that you will assign to each view.
+ * - circularflow_radiusInDP = "90,100,110,120,130"
. Receive the radios in DP that you will assign to each view.
+ *
+ *
+ * Example in XML:
+ *
+ *
+ * DEFAULT radius - If you add a view and don't set its radius, the default value will be 0.
+ * DEFAULT angles - If you add a view and don't set its angle, the default value will be 0.
+ *
+ * Recommendation - always set radius and angle for all views in constraint_referenced_ids
+ *
+ * */
+
+public class CircularFlow extends VirtualLayout {
+ private static final String TAG = "CircularFlow";
+ ConstraintLayout mContainer;
+ int mViewCenter;
+ private static final int DEFAULT_RADIUS = 0;
+ private static final float DEFAULT_ANGLE = 0F;
+ /**
+ * @hide
+ */
+ private float[] mAngles = new float[32];
+
+ /**
+ * @hide
+ */
+ private int[] mRadius = new int[32];
+
+ /**
+ * @hide
+ */
+ private int mCountRadius;
+
+ /**
+ * @hide
+ */
+ private int mCountAngle;
+
+ /**
+ * @hide
+ */
+ private String mReferenceAngles;
+
+ /**
+ * @hide
+ */
+ private String mReferenceRadius;
+
+ public CircularFlow(Context context) {
+ super(context);
+ }
+
+ public CircularFlow(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CircularFlow(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public int[] getRadius() {
+ return Arrays.copyOf(mRadius, mCountRadius);
+ }
+
+
+ public float[] getAngles() {
+ return Arrays.copyOf(mAngles, mCountAngle);
+ }
+
+
+ @Override
+ protected void init(AttributeSet attrs) {
+ super.init(attrs);
+ if (attrs != null) {
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ConstraintLayout_Layout);
+ final int N = a.getIndexCount();
+
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ if (attr == R.styleable.ConstraintLayout_Layout_circularflow_viewCenter) {
+ mViewCenter = a.getResourceId(attr, 0);
+ } else if (attr == R.styleable.ConstraintLayout_Layout_circularflow_angles) {
+ mReferenceAngles = a.getString(attr);
+ setAngles(mReferenceAngles);
+ } else if (attr == R.styleable.ConstraintLayout_Layout_circularflow_radiusInDP) {
+ mReferenceRadius = a.getString(attr);
+ setRadius(mReferenceRadius);
+ }
+ }
+ a.recycle();
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mReferenceAngles != null) {
+ setAngles(mReferenceAngles);
+ }
+ if (mReferenceRadius != null) {
+ setRadius(mReferenceRadius);
+ }
+ anchorReferences();
+ }
+
+ private void anchorReferences() {
+ mContainer = (ConstraintLayout) getParent();
+ ConstraintSet c = new ConstraintSet();
+ c.clone(mContainer);
+ for (int i = 0; i <= mCount; i++) {
+ int id = mIds[i];
+ View view = mContainer.getViewById(id);
+
+ if (view != null) {
+ int radius = DEFAULT_RADIUS;
+ float angle = DEFAULT_ANGLE;
+
+ if (i < getRadius().length){
+ radius = getRadius()[i];
+ } else {
+ Log.e("CircularFlow", "Added radius to view with id: " + mMap.get(view.getId()));
+ }
+
+ if (i < getAngles().length){
+ angle = getAngles()[i];
+ } else {
+ Log.e("CircularFlow", "Added angle to view with id: " + mMap.get(view.getId()));
+ }
+ c.constrainCircle(view.getId(), mViewCenter, radius, angle);
+ }
+ }
+ c.applyTo(mContainer);
+ applyLayoutFeatures();
+ }
+
+ /**
+ * Add a view to the CircularFlow. The referenced view need to be a child of the container parent.
+ * The view also need to have its id set in order to be added.
+ * The views previous need to have its radius and angle set in order to be added correctly a new view.
+ * @param view
+ * @param radius
+ * @param angle
+ * @return
+ */
+ public void addViewToCircularFlow(View view, int radius, float angle) {
+ if (containsId(view.getId())){
+ return;
+ }
+ addView(view);
+ mCountAngle++;
+ mAngles = getAngles();
+ mAngles[mCountAngle - 1] = angle;
+ mCountRadius++;
+ mRadius = getRadius();
+ mRadius[mCountRadius - 1] = (int) (radius * myContext.getResources().getDisplayMetrics().density);
+ anchorReferences();
+ }
+
+ @Override
+ public int removeView(View view) {
+ int index = super.removeView(view);
+ if (index == -1) {
+ return index;
+ }
+ ConstraintSet c = new ConstraintSet();
+ c.clone(mContainer);
+ c.clear(view.getId(), ConstraintSet.CIRCLE_REFERENCE);
+ c.applyTo(mContainer);
+
+ if (index < mAngles.length) {
+ mAngles = removeAngle(mAngles, index);
+ mCountAngle--;
+ }
+ if (index < mRadius.length) {
+ mRadius = removeRadius(mRadius, index);
+ mCountRadius--;
+ }
+ anchorReferences();
+ return index;
+ }
+
+ /**
+ * @hide
+ */
+ private float[] removeAngle(float[] angles, int index) {
+ if (angles == null
+ || index < 0
+ || index >= mCountAngle) {
+ return angles;
+ }
+
+ return removeElementFromArray(angles, index);
+ }
+
+ /**
+ * @hide
+ */
+ private int[] removeRadius(int[] radius, int index) {
+ if (radius == null
+ || index < 0
+ || index >= mCountRadius) {
+ return radius;
+ }
+
+ return removeElementFromArray(radius, index);
+ }
+
+ /**
+ * @hide
+ */
+ private void setAngles(String idList) {
+ if (idList == null) {
+ return;
+ }
+ int begin = 0;
+ mCountAngle = 0;
+ while (true) {
+ int end = idList.indexOf(',', begin);
+ if (end == -1) {
+ addAngle(idList.substring(begin).trim());
+ break;
+ }
+ addAngle(idList.substring(begin, end).trim());
+ begin = end + 1;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ private void setRadius(String idList) {
+ if (idList == null) {
+ return;
+ }
+ int begin = 0;
+ mCountRadius = 0;
+ while (true) {
+ int end = idList.indexOf(',', begin);
+ if (end == -1) {
+ addRadius(idList.substring(begin).trim());
+ break;
+ }
+ addRadius(idList.substring(begin, end).trim());
+ begin = end + 1;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ private void addAngle(String angleString) {
+ if (angleString == null || angleString.length() == 0) {
+ return;
+ }
+ if (myContext == null) {
+ return;
+ }
+ if (mAngles == null) {
+ return;
+ }
+
+ if (mCountAngle + 1 > mAngles.length) {
+ mAngles = Arrays.copyOf(mAngles, mAngles.length * 2);
+ }
+ mAngles[mCountAngle] = Integer.parseInt(angleString);
+ mCountAngle++;
+ }
+
+ /**
+ * @hide
+ */
+ private void addRadius(String radiusString) {
+ if (radiusString == null || radiusString.length() == 0) {
+ return;
+ }
+ if (myContext == null) {
+ return;
+ }
+ if (mRadius == null) {
+ return;
+ }
+
+ if (mCountRadius + 1 > mRadius.length) {
+ mRadius = Arrays.copyOf(mRadius, mRadius.length * 2);
+ }
+
+ mRadius[mCountRadius] = (int) (Integer.parseInt(radiusString) * myContext.getResources().getDisplayMetrics().density);
+ mCountRadius++;
+ }
+
+ public static int[] removeElementFromArray(int[] array, int index) {
+ int[] newArray = new int[array.length - 1];
+
+ for (int i = 0, k = 0; i < array.length; i++) {
+ if (i == index) {
+ continue;
+ }
+ newArray[k++] = array[i];
+ }
+ return newArray;
+ }
+
+ public static float[] removeElementFromArray(float[] array, int index) {
+ float[] newArray = new float[array.length - 1];
+
+ for (int i = 0, k = 0; i < array.length; i++) {
+ if (i == index) {
+ continue;
+ }
+ newArray[k++] = array[i];
+ }
+ return newArray;
+ }
+}
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintHelper.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintHelper.java
index b799e4b37..0ffabaaf5 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintHelper.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintHelper.java
@@ -77,7 +77,7 @@ public abstract class ConstraintHelper extends View {
*/
private View[] mViews = null;
- private HashMap mMap = new HashMap<>();
+ protected HashMap mMap = new HashMap<>();
public ConstraintHelper(Context context) {
super(context);
@@ -156,15 +156,18 @@ public void addView(View view) {
* Remove a given view from the helper.
*
* @param view
+ * @return index of view removed
*/
- public void removeView(View view) {
+ public int removeView(View view) {
+ int index = -1;
int id = view.getId();
if (id == -1) {
- return;
+ return index;
}
mReferenceIds = null;
for (int i = 0; i < mCount; i++) {
if (mIds[i] == id) {
+ index = i;
for (int j = i; j < mCount -1; j++) {
mIds[j] = mIds[j + 1];
}
@@ -174,6 +177,7 @@ public void removeView(View view) {
}
}
requestLayout();
+ return index;
}
/**
@@ -410,6 +414,7 @@ protected void setIds(String idList) {
begin = end + 1;
}
}
+
/**
* @hide
*/
@@ -599,4 +604,15 @@ public void setTag(int key, Object tag) {
addRscID(key);
}
}
+
+ public boolean containsId(final int id) {
+ boolean result = false;
+ for(int i : mIds){
+ if(i == id){
+ result = true;
+ break;
+ }
+ }
+ return result;
+ }
}
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
index 768339684..eb8ae5da4 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
@@ -2156,6 +2156,11 @@ public static class LayoutParams extends ViewGroup.LayoutParams {
*/
public static final int END = 7;
+ /**
+ * Circle reference from a view.
+ */
+ public static final int CIRCLE = 8;
+
/**
* Set matchConstraintDefault* default to the wrap content size.
* Use to set the matchConstraintDefaultWidth and matchConstraintDefaultHeight
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
index 84a594bf6..22c38491b 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
@@ -210,6 +210,11 @@ public class ConstraintSet {
*/
public static final int END = ConstraintLayout.LayoutParams.END;
+ /**
+ * Circle reference from a view.
+ */
+ public static final int CIRCLE_REFERENCE = ConstraintLayout.LayoutParams.CIRCLE;
+
/**
* Chain spread style
*/
@@ -3031,6 +3036,11 @@ public void clear(int viewId, int anchor) {
constraint.layout.endMargin = Layout.UNSET;
constraint.layout.goneEndMargin = Layout.UNSET;
break;
+ case CIRCLE_REFERENCE:
+ constraint.layout.circleAngle = Layout.UNSET;
+ constraint.layout.circleRadius = Layout.UNSET;
+ constraint.layout.circleConstraint = Layout.UNSET;
+ break;
default:
throw new IllegalArgumentException("unknown constraint");
}
diff --git a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
index 57c621c34..08757b2f3 100644
--- a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
+++ b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
@@ -261,6 +261,12 @@
+
+
+
+
+
+
@@ -506,6 +512,10 @@
+
+
+
+
diff --git a/projects/CarouselExperiments/app/src/main/AndroidManifest.xml b/projects/CarouselExperiments/app/src/main/AndroidManifest.xml
index 6385bcf1c..20ce0dc01 100644
--- a/projects/CarouselExperiments/app/src/main/AndroidManifest.xml
+++ b/projects/CarouselExperiments/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DeveloperTemplate">
+
@@ -16,7 +17,7 @@
-
+
\ No newline at end of file
diff --git a/projects/CarouselExperiments/app/src/main/java/androidx/constraintlayout/experiments/CircularFlowDemoActivity.kt b/projects/CarouselExperiments/app/src/main/java/androidx/constraintlayout/experiments/CircularFlowDemoActivity.kt
new file mode 100644
index 000000000..9859e1a41
--- /dev/null
+++ b/projects/CarouselExperiments/app/src/main/java/androidx/constraintlayout/experiments/CircularFlowDemoActivity.kt
@@ -0,0 +1,49 @@
+package androidx.constraintlayout.experiments
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+import androidx.constraintlayout.helper.widget.CircularFlow
+
+class CircularFlowDemoActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_circular_flow_demo)
+
+ findViewById(R.id.view6).setOnClickListener {
+ findViewById(R.id.circularFlow).addViewToCircularFlow(
+ it, 130, 160F
+ )
+ }
+
+ findViewById(R.id.view7).setOnClickListener {
+ findViewById(R.id.circularFlow).addViewToCircularFlow(
+ it, 140, 200F
+ )
+ }
+
+ findViewById(R.id.view8).setOnClickListener {
+ findViewById(R.id.circularFlow).addViewToCircularFlow(
+ it, 150, 240F
+ )
+ }
+
+ findViewById(R.id.view2).setOnClickListener {
+ findViewById(R.id.circularFlow).removeView(
+ it
+ )
+ }
+
+ findViewById(R.id.view3).setOnClickListener {
+ findViewById(R.id.circularFlow).removeView(
+ it
+ )
+ }
+
+ findViewById(R.id.view4).setOnClickListener {
+ findViewById(R.id.circularFlow).removeView(
+ it
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/CarouselExperiments/app/src/main/java/androidx/constraintlayout/experiments/MainActivity.java b/projects/CarouselExperiments/app/src/main/java/androidx/constraintlayout/experiments/MainActivity.java
index a00df5da7..7196c5555 100644
--- a/projects/CarouselExperiments/app/src/main/java/androidx/constraintlayout/experiments/MainActivity.java
+++ b/projects/CarouselExperiments/app/src/main/java/androidx/constraintlayout/experiments/MainActivity.java
@@ -75,7 +75,8 @@ public class MainActivity extends AppCompatActivity {
// Array from Activities with more examples
Class activitiesDemo[] = {
- CarouselHelperActivity.class
+ CarouselHelperActivity.class,
+ CircularFlowDemoActivity.class
};
////////////////////////////////////////////////////////////////
diff --git a/projects/CarouselExperiments/app/src/main/res/layout/activity_circular_flow_demo.xml b/projects/CarouselExperiments/app/src/main/res/layout/activity_circular_flow_demo.xml
new file mode 100644
index 000000000..0dba5f1ad
--- /dev/null
+++ b/projects/CarouselExperiments/app/src/main/res/layout/activity_circular_flow_demo.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file