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: + * + * + * 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