Android Constraint Layout – Circular Positioning

Filed Under: Android

In this tutorial, we’ll be discussing a cool attribute present in Constraint Layout, namely circular positioning. We’ll be developing a custom clock UI in our Android Application using the same.

Android Constraint Layout

We’ve discussed Constraint Layout in this tutorial.

It’s a great layout and provides lots of points to set views relative to one another.

With the introduction of ConstraintLayout 1.1, we can now set positions based on angles and radius too. This is termed as circular positioning.

Circular Postioning

A picture is worth a thousands of words:

android constraint layout circular positioning

From Google Docs

Circular Positioning consists of the following three attributes that need to be defined in the ConstraintLayout child views:

  • layout_constraintCircle : This would be defined in the View B. It would be set to the id of A.
  • layout_constraintCircleRadius : The distance between the center of the two views.
  • layout_constraintCircleAngle : The angle defines the circular position of B with respect to A. This is set in degrees( 0 to 360). An angle of 0 means the view B is vertically above A.

We can create a complete circle with views positioned at different angles.

In the following section, we’ll create a custom clock in which TextViews would be circularly positioned around an ImageView.

Each text view would represent an hour of the clock. Also, we’ll set another ImageView as the seconds’ pointer that would animate and update the circular position based on the values defined.

Project Structure

android constraint layout circle project structure

Code

Let’s draw a circle shape that would be used as the clock pointer.


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid android:color="#ff00ff" />

    <size
        android:width="8dp"
        android:height="8dp" />
</shape>

The code for the activity_main.xml layout file is given below:


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imgClock"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/checkbox_off_background"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/imgPointer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/circle"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="0"
        app:layout_constraintCircleRadius="70dp" />


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="12"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="0"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="30"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="2"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="60"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="3"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="90"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="4"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="120"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="5"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="150"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="6"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="180"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="7"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="210"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="8"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="240"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="9"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="270"
        app:layout_constraintCircleRadius="90dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="10"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="300"
        app:layout_constraintCircleRadius="90dp" />


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="11"
        android:textColor="@android:color/black"
        app:layout_constraintCircle="@+id/imgClock"
        app:layout_constraintCircleAngle="330"
        app:layout_constraintCircleRadius="90dp" />


</android.support.constraint.ConstraintLayout>

The code for the MainActivity.java class is given below:


package com.journaldev.androidconstraintcircles;

import android.animation.ValueAnimator;
import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.Toast;

import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity {

    ImageView imgPointer, imgClock;
    ValueAnimator clockAnimator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imgPointer = findViewById(R.id.imgPointer);
        imgClock = findViewById(R.id.imgClock);

        clockAnimator = animatePointer(TimeUnit.SECONDS.toMillis(60));


        imgClock.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (clockAnimator.isPaused()) {
                    clockAnimator.resume();
                    Toast.makeText(getApplicationContext(), "Resumed", Toast.LENGTH_SHORT).show();
                } else if (clockAnimator.isRunning()) {
                    Toast.makeText(getApplicationContext(), "Paused", Toast.LENGTH_SHORT).show();
                    clockAnimator.pause();
                } else
                    clockAnimator.start();

            }
        });
    }

    
    private ValueAnimator animatePointer(long orbitDuration) {
        ValueAnimator anim = ValueAnimator.ofInt(0, 359);

        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int val = (Integer) valueAnimator.getAnimatedValue();
                ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imgPointer.getLayoutParams();
                layoutParams.circleAngle = val;
                imgPointer.setLayoutParams(layoutParams);
            }
        });
        anim.setDuration(orbitDuration);
        anim.setInterpolator(new LinearInterpolator());
        anim.setRepeatMode(ValueAnimator.RESTART);
        anim.setRepeatCount(ValueAnimator.INFINITE);

        return anim;
    }
}

In the above code, we’ve defined a ValueAnimator class object. It is used to animate a view based on values.

We update the imgPointer constraint circular position angle relative to the center ImageView as the ValueAnimator’s interval updates.

Here it happens each second so it gives you the impression that the imgPointer is a seconds hand of a clock!

However, we have a loophole in the above code.

The Animation isn’t tied to the Activity’s lifecycle. So if the application goes in background, it will give rise to memory leaks. For that we can disable and enable the ValueAnimator animations in the onResume(), onPause() and onDestroy() methods.

Add the following snippet in your MainActivity above:


@Override
    protected void onResume() {
        super.onResume();
        if (clockAnimator != null) {
            if (clockAnimator.isPaused()) {
                clockAnimator.resume();
            }
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (clockAnimator.isRunning()) {
            clockAnimator.pause();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (clockAnimator.isRunning()) {
            clockAnimator.cancel();
        }
    }

The output of the above application in action is given below:
android constraint layout circle clock app output

This brings an end to this tutorial. You can download the project from the link below:

You can also find the project at the Github Repository:

Comments

  1. Noura Abdelnoor says:

    This view is not constrained. It only has designtime positions, so it will jump to (0,0) at runtime unless you add the constraints more… (Ctrl+F1)
    this appear once i create image veiw

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages