Programmatic Views how to set unique id's?
Asked Answered
G

5

37

I am creating in my app bunch of programmatic Views. As it appeared to be they all by default have the same id=-1. In order to work with them I need to generate unique id's.

I have tried several approaches - random number generation and based on current time, but anyway there's no 100% guarantee that different Views will have different id's

Just wondering is there any more reliable way to generate unique ones? Probably there's special method/class?

Grating answered 22/7, 2011 at 13:22 Comment(2)
I think the best way is to keep a int counter and increment when you add a view and set that as idNickinickie
Possible duplicate of Android: View.setID(int id) programmatically - how to avoid ID conflicts?Exemplificative
M
49

Just an addition to the answer of @phantomlimb,

while View.generateViewId() require API Level >= 17,
this tool is compatibe with all API.

according to current API Level,
it decide weather using system API or not.

so you can use ViewIdGenerator.generateViewId() and View.generateViewId() in the same time and don't worry about getting same id

import java.util.concurrent.atomic.AtomicInteger;

import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;

/**
 * {@link View#generateViewId()}要求API Level >= 17,而本工具类可兼容所有API Level
 * <p>
 * 自动判断当前API Level,并优先调用{@link View#generateViewId()},即使本工具类与{@link View#generateViewId()}
 * 混用,也能保证生成的Id唯一
 * <p>
 * =============
 * <p>
 * while {@link View#generateViewId()} require API Level >= 17, this tool is compatibe with all API.
 * <p>
 * according to current API Level, it decide weather using system API or not.<br>
 * so you can use {@link ViewIdGenerator#generateViewId()} and {@link View#generateViewId()} in the
 * same time and don't worry about getting same id
 * 
 * @author [email protected]
 */
public class ViewIdGenerator {
    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    @SuppressLint("NewApi")
    public static int generateViewId() {

        if (Build.VERSION.SDK_INT < 17) {
            for (;;) {
                final int result = sNextGeneratedId.get();
                // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
                int newValue = result + 1;
                if (newValue > 0x00FFFFFF)
                    newValue = 1; // Roll over to 1, not 0.
                if (sNextGeneratedId.compareAndSet(result, newValue)) {
                    return result;
                }
            }
        } else {
            return View.generateViewId();
        }

    }
}
Mexico answered 8/1, 2014 at 15:47 Comment(5)
Actually, this is incorrect - you use a different AtomicInteger counter to that of View.generateViewId(), so you cannot use this class and View.generateViewId() interchangeably as you assert in the documentation of your class. One would need to use this class exclusively.Buzzell
@Buzzell if you can use ViewIdGenerator.generateViewId() and View.generateViewId() interchangeably, that means Build.VERSION.SDK_INT >= 17 ,so the code if (Build.VERSION.SDK_INT < 17) {…} will never run. actually, it always use View.generateViewId() in that case. :)Mexico
Ha, it's nice to see some Chinese comments in the code:)Prospective
@reegan29 since the Android official API View.generateViewId() has nothing as reset, this code snippet also do not haveMexico
Source code is here.Wimer
P
56

Just want to add to Kaj's answer, from API level 17, you can call

View.generateViewId()

then use the View.setId(int) method.

In case you need it for targets lower than level 17, here is its internal implementation in View.java you can use directly in your project:

private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

/**
 * Generate a value suitable for use in {@link #setId(int)}.
 * This value will not collide with ID values generated at build time by aapt for R.id.
 *
 * @return a generated ID value
 */
public static int generateViewId() {
    for (;;) {
        final int result = sNextGeneratedId.get();
        // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
        int newValue = result + 1;
        if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
        if (sNextGeneratedId.compareAndSet(result, newValue)) {
            return result;
        }
    }
}

ID number larger than 0x00FFFFFF is reserved for static views defined in the /res xml files. (Most likely 0x7f****** from the R.java in my projects.)

From the code, somehow Android doesn't want you to use 0 as a view's id, and it needs to be flipped before 0x01000000 to avoid the conflits with static resource IDs.

Prospective answered 15/3, 2013 at 22:19 Comment(1)
It does not want you to use 0 as a view id because this is universal to represent an invalid resource id when querying for a resource at runtime.Haematic
M
49

Just an addition to the answer of @phantomlimb,

while View.generateViewId() require API Level >= 17,
this tool is compatibe with all API.

according to current API Level,
it decide weather using system API or not.

so you can use ViewIdGenerator.generateViewId() and View.generateViewId() in the same time and don't worry about getting same id

import java.util.concurrent.atomic.AtomicInteger;

import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;

/**
 * {@link View#generateViewId()}要求API Level >= 17,而本工具类可兼容所有API Level
 * <p>
 * 自动判断当前API Level,并优先调用{@link View#generateViewId()},即使本工具类与{@link View#generateViewId()}
 * 混用,也能保证生成的Id唯一
 * <p>
 * =============
 * <p>
 * while {@link View#generateViewId()} require API Level >= 17, this tool is compatibe with all API.
 * <p>
 * according to current API Level, it decide weather using system API or not.<br>
 * so you can use {@link ViewIdGenerator#generateViewId()} and {@link View#generateViewId()} in the
 * same time and don't worry about getting same id
 * 
 * @author [email protected]
 */
public class ViewIdGenerator {
    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    @SuppressLint("NewApi")
    public static int generateViewId() {

        if (Build.VERSION.SDK_INT < 17) {
            for (;;) {
                final int result = sNextGeneratedId.get();
                // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
                int newValue = result + 1;
                if (newValue > 0x00FFFFFF)
                    newValue = 1; // Roll over to 1, not 0.
                if (sNextGeneratedId.compareAndSet(result, newValue)) {
                    return result;
                }
            }
        } else {
            return View.generateViewId();
        }

    }
}
Mexico answered 8/1, 2014 at 15:47 Comment(5)
Actually, this is incorrect - you use a different AtomicInteger counter to that of View.generateViewId(), so you cannot use this class and View.generateViewId() interchangeably as you assert in the documentation of your class. One would need to use this class exclusively.Buzzell
@Buzzell if you can use ViewIdGenerator.generateViewId() and View.generateViewId() interchangeably, that means Build.VERSION.SDK_INT >= 17 ,so the code if (Build.VERSION.SDK_INT < 17) {…} will never run. actually, it always use View.generateViewId() in that case. :)Mexico
Ha, it's nice to see some Chinese comments in the code:)Prospective
@reegan29 since the Android official API View.generateViewId() has nothing as reset, this code snippet also do not haveMexico
Source code is here.Wimer
I
26

Since support library 27.1.0 there's generateViewId() in ViewCompat

ViewCompat.generateViewId()

Ion answered 10/8, 2018 at 9:23 Comment(0)
S
12

Create a singleton class, that has an atomic Integer. Bump the integer, and return the value when you need a view id.

The id will be unique during the execution of your process, but wil reset when your process is restarted.

public class ViewId {

    private static ViewId INSTANCE = new ViewId();

    private AtomicInteger seq;

    private ViewId() {
        seq = new AtomicInteger(0);
    }

    public int getUniqueId() {
        return seq.incrementAndGet();
    }

    public static ViewId getInstance() {
        return INSTANCE;
    }
}

Note that the id might not be unique, if there already are views that have ids in the view 'graph'. You could try to start with a number that is Integer.MAX_VALUE, and decrease it instead of going from 1 -> MAX_VALUE

Sphygmomanometer answered 22/7, 2011 at 13:30 Comment(4)
Short of checking every ID that is generated in the R.java file, there's no way to tell if an ID is unique or not (from what I can tell). There are certainly cases where Android will muff things up because of ID clashes. The best way I found was to use Integer.MAX_VALUE and decrease the value every time you need a new ID, as you mentioned.Rumney
this is a way.. but as Xiaochao Yang mentioned, from API 17 you can use the method View.generateViewId()Keel
Can you answer me.This is based on your answer...#32382178Labored
Can you explain bit more in which case this answer won't generate unique Id?Verditer
Y
3

Regarding the fallback solution for API<17, I see that suggested solutions start generating IDs starting from 0 or 1. The View class has another instance of generator, and also starts counting from number one, which will result in both your and View's generator generating the same IDs, and you will end up having different Views with same IDs in your View hierarchy. Unfortunately there is no a good solution for this but it's a hack that should be well documented:

public class AndroidUtils {

/**
 *  Unique view id generator, like the one used in {@link View} class for view id generation.
 *  Since we can't access the generator within the {@link View} class before API 17, we create
 *  the same generator here. This creates a problem of two generator instances not knowing about
 *  each other, and we need to take care that one does not generate the id already generated by other one.
 *
 *  We know that all integers higher than 16 777 215 are reserved for aapt-generated identifiers
 *  (source: {@link View#generateViewId()}, so we make sure to never generate a value that big.
 *  We also know that generator within the {@link View} class starts at 1.
 *  We set our generator to start counting at 15 000 000. This gives us enough space
 *  (15 000 000 - 16 777 215), while making sure that generated IDs are unique, unless View generates
 *  more than 15M IDs, which should never happen.
 */
private static final AtomicInteger viewIdGenerator = new AtomicInteger(15000000);

/**
 * Generate a value suitable for use in {@link View#setId(int)}.
 * This value will not collide with ID values generated at build time by aapt for R.id.
 *
 * @return a generated ID value
 */
public static int generateViewId() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
        return generateUniqueViewId();
    } else {
        return View.generateViewId();
    }
}

private static int generateUniqueViewId() {
    while (true) {
        final int result = viewIdGenerator.get();
        // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
        int newValue = result + 1;
        if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
        if (viewIdGenerator.compareAndSet(result, newValue)) {
            return result;
        }
    }
}

}
Yung answered 3/9, 2016 at 13:26 Comment(2)
Good point about a possible conflict between the app's custom generator and the internal generator in the View class. Have you checked the actual logic of View.generateViewId() for API 16? I've not found a copy of the source, so just hoping it is basically the same as more recent API versions.Apostolic
Haven't found the source code. But I experienced conflicts when using View.generateViewId() even on new APIs, so I modified the code to always use my own generateUniqueViewId method.Yung

© 2022 - 2024 — McMap. All rights reserved.