Làm cách nào để sử dụng InputFilter để giới hạn các ký tự trong EditText trong Android?


171

Tôi muốn giới hạn ký tự chỉ 0-9, az, AZ và phím cách. Đặt inputtype Tôi có thể giới hạn ở các chữ số nhưng tôi không thể tìm ra các cách của Inputfilter xem qua các tài liệu.

Câu trả lời:


186

Tôi tìm thấy điều này trên một diễn đàn khác. Hoạt động như một nhà vô địch.

InputFilter filter = new InputFilter() {
    public CharSequence filter(CharSequence source, int start, int end,
            Spanned dest, int dstart, int dend) {
        for (int i = start; i < end; i++) {
            if (!Character.isLetterOrDigit(source.charAt(i))) {
                return "";
            }
        }
        return null;
    }
};
edit.setFilters(new InputFilter[] { filter });

58
thực tế nó không hoạt động tốt trong các Android mới hơn (như 4.0+). Họ giới thiệu các đề xuất từ ​​điển trên bàn phím. Khi bạn nhập một từ phổ biến (giả sử "the") theo sau là một ký tự không hợp lệ cho bộ lọc này (giả sử "-"), toàn bộ từ sẽ bị xóa và sau khi bạn nhập một ký tự khác (ngay cả những ký tự được phép, như "blah") bộ lọc trả về "" và không có ký tự nào hiển thị trong trường. Điều này là do phương thức này có một SpannableStringBuilder trong tham số nguồn với "the-blah" trong đó và các tham số start / end bao trùm toàn bộ chuỗi đầu vào ... Xem câu trả lời của tôi để có giải pháp tốt hơn.
Łukasz Sromek

4
Trong ví dụ đó, nơi nó trả về "", tôi nghĩ rằng nó sẽ trả về văn bản sẽ được hiển thị. tức là bạn nên loại bỏ các ký tự không hợp lệ và trả về chuỗi bạn muốn hiển thị. developer.android.com/reference/android/widget/ từ , android.view.KeyEvent)
Andrew Mackenzie

+1 Thực sự là một câu trả lời tuyệt vời. Character.isLetterOrDigit () tất cả các phương thức này đều rất hữu ích.
Atul Bhardwaj

@AndrewMackenzie Nếu ký tự đầu vào, ví dụ: dấu phẩy- ',', không hợp pháp và tôi muốn xóa nó, làm thế nào để sửa mã ở trên.
twlkyao

! Character.isLetterOrDigit (source.charAt (i)) &&! Character.isSpaceChar (source.charAt (i)) để cho phép khoảng trắng
Leon

139

InputFilters là một chút phức tạp trong các phiên bản Android hiển thị các đề xuất từ ​​điển. Đôi khi bạn nhận được một SpannableStringBuilder, đôi khi một đồng bằng Stringtrong sourcetham số.

Sau đây InputFilternên làm việc. Hãy cải thiện mã này!

new InputFilter() {
    @Override
    public CharSequence filter(CharSequence source, int start, int end,
            Spanned dest, int dstart, int dend) {

        if (source instanceof SpannableStringBuilder) {
            SpannableStringBuilder sourceAsSpannableBuilder = (SpannableStringBuilder)source;
            for (int i = end - 1; i >= start; i--) { 
                char currentChar = source.charAt(i);
                 if (!Character.isLetterOrDigit(currentChar) && !Character.isSpaceChar(currentChar)) {    
                     sourceAsSpannableBuilder.delete(i, i+1);
                 }     
            }
            return source;
        } else {
            StringBuilder filteredStringBuilder = new StringBuilder();
            for (int i = start; i < end; i++) { 
                char currentChar = source.charAt(i);
                if (Character.isLetterOrDigit(currentChar) || Character.isSpaceChar(currentChar)) {    
                    filteredStringBuilder.append(currentChar);
                }     
            }
            return filteredStringBuilder.toString();
        }
    }
}

2
Có một lý do tại sao bạn không muốn tiếp tục nguồn? Bạn có thấy có gì sai khi chỉ làm điều này không (để chỉ cho phép chữ và số cộng với một vài ký tự đặc biệt): String replacement = source.subSequence(start, end).toString(); return replacement.replaceAll("[^A-Za-z0-9_\\-@]", "");
Splash

3
Điều này không tính đến một vấn đề trong đó lặp lại văn bản gợi ý từ điển xuất hiện. @serwus xác định rằng trong câu trả lời của họ. Về cơ bản, bạn nên trả về null nếu không có sửa đổi nào được thực hiện trong cả hai trường hợp.
hooby3dfx

3
Mã này hoạt động hoàn hảo như lukasz đã nói (trong câu trả lời ở trên) nhưng tôi đang gặp một số vấn đề với các từ không phải từ điển. Khi tôi gõ chiru, nó hiển thị 3 lần như chiruchiruchiru. Làm thế nào để giải quyết nó? và nó cũng mất khoảng trắng nhưng làm thế nào để hạn chế bên cạnh khoảng trắng tiếp theo?
chiru

3
Vì một số lý do, khi source instanceof SpannableStringBuildervào AB cho tôi AAB như khi thử câu trả lời trước. May mắn thay, tôi đã có thể làm việc xung quanh nó bằng cách sử dụng giải pháp @florian bên dưới.
Guillaume

1
@ hooby3dfx là hoàn toàn đúng. Mặc dù cuối cùng nó cũng có được chuỗi đúng, nhưng nó sẽ gây rối khi bạn sử dụng từ điển / hoàn thành từ.
Aravind

107

dễ dàng hơn nhiều:

<EditText
    android:inputType="text"
    android:digits="0,1,2,3,4,5,6,7,8,9,*,qwertzuiopasdfghjklyxcvbnm" />

9
Mặc dù đây có vẻ là một câu trả lời hoàn hảo, nhưng có một vấn đề theo các tài liệu: "Đối với tất cả các triển khai của KeyListener, lớp này chỉ liên quan đến bàn phím phần cứng. Các phương thức nhập phần mềm không có nghĩa vụ phải kích hoạt các phương thức trong lớp này." Tôi nghĩ rằng InputFilter có lẽ là cách tốt hơn, mặc dù phức tạp hơn, để đi.
Craig B

19
Giải pháp tuyệt vời chỉ muốn thêm Bạn không cần phải đưa ra ","giữa. Bạn có thể sử dụng một cái gì đó như thế này"0123456789qwertzuiopasdfghjklyxcvbnmQWERTZUIOPASDFGHJKLYXCVBNM"
DeltaCap019

26
Không phải là một giải pháp đa ngôn ngữ
AAverin

Nếu bạn có kế hoạch sử dụng bản dịch trong ứng dụng của bạn. KHÔNG SỬ DỤNG GIẢI PHÁP NÀY!
Grantespo

Có vẻ như điều này có thể không hoạt động imeOptions="actionNext", v.v.
Pete Doyle

68

Không có câu trả lời được đăng đã làm việc cho tôi. Tôi đến với giải pháp của riêng tôi:

InputFilter filter = new InputFilter() {
    @Override
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        boolean keepOriginal = true;
        StringBuilder sb = new StringBuilder(end - start);
        for (int i = start; i < end; i++) {
            char c = source.charAt(i);
            if (isCharAllowed(c)) // put your condition here
                sb.append(c);
            else
                keepOriginal = false;
        }
        if (keepOriginal)
            return null;
        else {
            if (source instanceof Spanned) {
                SpannableString sp = new SpannableString(sb);
                TextUtils.copySpansFrom((Spanned) source, start, sb.length(), null, sp, 0);
                return sp;
            } else {
                return sb;
            }           
        }
    }

    private boolean isCharAllowed(char c) {
        return Character.isLetterOrDigit(c) || Character.isSpaceChar(c);
    }
}
editText.setFilters(new InputFilter[] { filter });

3
Đây là câu trả lời duy nhất thực sự có cách tiếp cận phù hợp để ngăn chặn việc lặp lại văn bản từ các đề xuất từ ​​điển! Upvote!
hooby3dfx 24/214

4
Điều duy nhất, EditTextcó thể đã có bộ lọc riêng, ví dụ: bộ lọc độ dài. Vì vậy, thay vì chỉ ghi đè các bộ lọc, rất có thể bạn muốn thêm các bộ lọc của mình vào các bộ lọc đã có.
Aleks N.

Điều này vẫn còn cập nhật? Đối với tôi Android 6.0.1, nó hoạt động trên bàn phím trên màn hình
XxGoliathusxX

2
Một vấn đề nhỏ với câu trả lời này (hoặc với cách thức hoạt động của cơ chế nhập liệu của Android) là việc backspace đôi khi khiến người dùng không hoạt động vì chúng nằm trong khoảng trống trên các ký tự không hợp lệ được nhập trước đó vẫn được giữ trong bộ đệm nguồn.
jk7

1
Vấn đề tương tự như với giải pháp của Łukasz Sromek: Một ký tự không hợp lệ theo dấu cách được thay thế bằng khoảng trắng.
Ingo Schalk-Schupp

25

Sử dụng công việc này 100% nhu cầu của bạn và rất đơn giản.

<EditText
android:inputType="textFilter"
android:digits="@string/myAlphaNumeric" />

Trong chuỗi DOM

<string name="myAlphaNumeric">abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789</string>

15

Để tránh các ký tự đặc biệt trong kiểu đầu vào

public static InputFilter filter = new InputFilter() {
    @Override
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        String blockCharacterSet = "~#^|$%*!@/()-'\":;,?{}=!$^';,?×÷<>{}€£¥₩%~`¤♡♥_|《》¡¿°•○●□■◇◆♧♣▲▼▶◀↑↓←→☆★▪:-);-):-D:-(:'(:O 1234567890";
        if (source != null && blockCharacterSet.contains(("" + source))) {
            return "";
        }
        return null;
    }
};

Bạn có thể đặt bộ lọc cho văn bản chỉnh sửa của mình như bên dưới

edtText.setFilters(new InputFilter[] { filter });

@sathya Bạn wc, Rất vui được giúp bạn :)

1
Không chặn bất kỳ nhân vật nào trong số này. ㋛ ☺ ت ت An
Anarchofascist

7

Ngoài câu trả lời được chấp nhận, cũng có thể sử dụng, ví dụ: android:inputType="textCapCharacters"như một thuộc tính <EditText>để chỉ chấp nhận các ký tự chữ hoa (và số).


5
android: inputType = "textCapChar character" không giới hạn việc sử dụng các ký tự khác như '., - "v.v.
Tvd

Nó cũng chỉ là một gợi ý cho phương thức nhập liệu. Nó không hạn chế những ký tự được phép nhập.
dcow

5

Vì một số lý do, hàm tạo của lớp android.text.LoginFilter nằm trong phạm vi gói, vì vậy bạn không thể trực tiếp mở rộng nó (mặc dù nó sẽ giống hệt với mã này). Nhưng bạn có thể mở rộng LoginFilter.UsernameFilterGeneric! Sau đó, bạn chỉ cần có điều này:

class ABCFilter extends LoginFilter.UsernameFilterGeneric {
    public UsernameFilter() {
        super(false); // false prevents not-allowed characters from being appended
    }

    @Override
    public boolean isAllowed(char c) {
        if ('A' <= c && c <= 'C')
            return true;
        if ('a' <= c && c <= 'c')
            return true;

        return false;
    }
}

Đây không phải là tài liệu thực sự, nhưng nó là một phần của lib cốt lõi, và nguồn rất đơn giản . Tôi đã sử dụng nó được một thời gian rồi, cho đến nay không có vấn đề gì, mặc dù tôi thừa nhận tôi đã không thử làm bất cứ điều gì phức tạp liên quan đến spannables.


5

Đúng vậy, cách tốt nhất để khắc phục nó trong chính Bố cục XML bằng cách sử dụng:

<EditText
android:inputType="text"
android:digits="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" />

như được chỉ ra bởi Florian Fröhlich, nó hoạt động tốt cho các chế độ xem văn bản.

<TextView
android:inputType="text"
android:digits="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" />

Chỉ cần một lời cảnh báo, các ký tự được đề cập trong android:digitssẽ chỉ được hiển thị, vì vậy hãy cẩn thận để không bỏ lỡ bất kỳ bộ ký tự nào :)


bạn cần xác định inputType = "textFilter" thì chỉ có nó mới hoạt động bình thường.
Shreyash Mahajan

@ShreyashMahajan, nó có phụ thuộc vào ứng dụng thiết bị / bàn phím không? Trong trường hợp của tôi inputTypekhông ảnh hưởng đến việc lọc.
CoolMind

4

Giải pháp đơn giản này hiệu quả với tôi khi tôi cần ngăn người dùng nhập các chuỗi trống vào EditText. Tất nhiên bạn có thể thêm nhiều ký tự:

InputFilter textFilter = new InputFilter() {

@Override

public CharSequence filter(CharSequence c, int arg1, int arg2,

    Spanned arg3, int arg4, int arg5) {

    StringBuilder sbText = new StringBuilder(c);

    String text = sbText.toString();

    if (text.contains(" ")) {    
        return "";   
    }    
    return c;   
    }   
};

private void setTextFilter(EditText editText) {

    editText.setFilters(new InputFilter[]{textFilter});

}

Làm thế nào để gọi giải pháp này?
zeek

1

Nếu bạn phân lớp InputFilter, bạn có thể tạo InputFilter của riêng mình để lọc bất kỳ ký tự không phải là số alpha.

Giao diện InputFilter có một phương thức filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)và nó cung cấp cho bạn tất cả thông tin bạn cần biết về các ký tự được nhập vào EditText mà nó được gán.

Khi bạn đã tạo InputFilter của riêng mình, bạn có thể gán nó cho EditText bằng cách gọi setFilters (...).

http://developer.android.com/reference/android/text/InputFilter.html#filter(java.lang.CharSequence , int, int, android.text.Spned, int, int)


1

Bỏ qua các công cụ span mà người khác đã xử lý, để xử lý đúng các đề xuất từ ​​điển, tôi thấy đoạn mã sau hoạt động.

Nguồn phát triển khi đề xuất tăng lên, vì vậy chúng tôi phải xem có bao nhiêu nhân vật thực sự mong đợi chúng tôi thay thế trước khi chúng tôi trả lại bất cứ điều gì.

Nếu chúng tôi không có bất kỳ ký tự không hợp lệ nào, hãy trả về null để thay thế mặc định xảy ra.

Mặt khác, chúng ta cần trích xuất các ký tự hợp lệ từ chuỗi con THỰC SỰ sẽ được đặt vào EditText.

InputFilter filter = new InputFilter() { 
    public CharSequence filter(CharSequence source, int start, int end, 
    Spanned dest, int dstart, int dend) { 

        boolean includesInvalidCharacter = false;
        StringBuilder stringBuilder = new StringBuilder();

        int destLength = dend - dstart + 1;
        int adjustStart = source.length() - destLength;
        for(int i=start ; i<end ; i++) {
            char sourceChar = source.charAt(i);
            if(Character.isLetterOrDigit(sourceChar)) {
                if(i >= adjustStart)
                     stringBuilder.append(sourceChar);
            } else
                includesInvalidCharacter = true;
        }
        return includesInvalidCharacter ? stringBuilder : null;
    } 
}; 

1

để ngăn chặn các từ trong edittext. tạo một lớp mà bạn có thể sử dụng bất cứ lúc nào

public class Wordfilter implements InputFilter
{
    @Override
    public CharSequence filter(CharSequence source, int start, int end,Spanned dest, int dstart, int dend) {
        // TODO Auto-generated method stub
        boolean append = false;
        String text = source.toString().substring(start, end);
        StringBuilder str = new StringBuilder(dest.toString());
        if(dstart == str.length())
        {
            append = true;
            str.append(text);
        }
        else
            str.replace(dstart, dend, text);
        if(str.toString().contains("aaaaaaaaaaaa/*the word here*/aaaaaaaa"))
        {
            if(append==true)
                return "";
            else
                return dest.subSequence(dstart, dend);
        }
        return null;
    }
}

1

Đầu tiên thêm vào strings.xml:

<string name="vin_code_mask">0123456789abcdefghjklmnprstuvwxyz</string>

XML :

android:digits="@string/vin_code_mask"

trong Kotlin:

edit_text.filters += InputFilter { source, start, end, _, _, _ ->
    val mask = getString(R.string.vin_code_mask)
    for (i in start until end) {
        if (!mask.contains(source[i])) {
            return@InputFilter ""
        }
    }
    null
}

Lạ, nhưng nó hoạt động kỳ lạ trên bàn phím mềm của trình giả lập.

Cảnh báo! Đoạn mã sau sẽ lọc tất cả các chữ cái và các ký hiệu khác trừ các chữ số cho bàn phím phần mềm. Chỉ bàn phím kỹ thuật số sẽ xuất hiện trên điện thoại thông minh.

edit_text.keyListener = DigitsKeyListener.getInstance(context.getString(R.string.vin_code_mask))

Tôi cũng thường được thiết lập maxLength, filters, inputType.


1

Đây là một chủ đề cũ, nhưng tất cả các giải pháp có mục đích đều có vấn đề (tùy thuộc vào thiết bị / phiên bản Android / Bàn phím).

PHƯƠNG PHÁP KHÁC NHAU

Vì vậy, cuối cùng tôi đã đi với một cách tiếp cận khác, thay vì sử dụng InputFiltertriển khai có vấn đề, tôi đang sử dụng TextWatcherTextChangedListenercủa EditText.

MÃ ĐẦY ĐỦ (VÍ DỤ)

editText.addTextChangedListener(new TextWatcher() {

    @Override
    public void afterTextChanged(Editable editable) {
        super.afterTextChanged(editable);

        String originalText = editable.toString();
        int originalTextLength = originalText.length();
        int currentSelection = editText.getSelectionStart();

        // Create the filtered text
        StringBuilder sb = new StringBuilder();
        boolean hasChanged = false;
        for (int i = 0; i < originalTextLength; i++) {
            char currentChar = originalText.charAt(i);
            if (isAllowed(currentChar)) {
                sb.append(currentChar);
            } else {
                hasChanged = true;
                if (currentSelection >= i) {
                    currentSelection--;
                }
            }
        }

        // If we filtered something, update the text and the cursor location
        if (hasChanged) {
            String newText = sb.toString();
            editText.setText(newText);
            editText.setSelection(currentSelection);
        }
    }

    private boolean isAllowed(char c) {
        // TODO: Add the filter logic here
        return Character.isLetter(c) || Character.isSpaceChar(c);
    }
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // Do Nothing
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // Do Nothing
    }
});

Lý do InputFilterkhông phải là một giải pháp tốt trong Android là vì nó phụ thuộc vào việc thực hiện bàn phím. Đầu vào Bàn phím đang được lọc trước khi đầu vào được chuyển đến EditText. Nhưng, bởi vì một số bàn phím có các triển khai khác nhau cho việc InputFilter.filter()gọi, nên đây là vấn đề.

Mặt khác, TextWatcherkhông quan tâm đến việc thực hiện bàn phím, nó cho phép chúng tôi tạo ra một giải pháp đơn giản và chắc chắn rằng nó sẽ hoạt động trên tất cả các thiết bị.


Đơn onTextChangedgiản chỉ cần một public voidở phía trước của nó.
Andy

Cảm ơn các bình luận. Đã sửa.
Eyal Biran

1

Tôi đã làm một cái gì đó như thế này để giữ cho nó đơn giản:

edit_text.filters = arrayOf(object : InputFilter {
    override fun filter(
        source: CharSequence?,
        start: Int,
        end: Int,
        dest: Spanned?,
        dstart: Int,
        dend: Int
    ): CharSequence? {
        return source?.subSequence(start, end)
            ?.replace(Regex("[^A-Za-z0-9 ]"), "")
    }
})

Bằng cách này, chúng tôi sẽ thay thế tất cả các ký tự không mong muốn trong phần mới của chuỗi nguồn bằng một chuỗi trống.

Các edit_textbiến làEditText đối tượng chúng ta đang đề cập đến.

Mã được viết bằng kotlin.


1
Cảm ơn! Giải pháp này hoạt động tốt khi gõ cũng như dán một văn bản.
CoolMind

0

Có thể sử dụng setOnKeyListener. Trong phương pháp này, chúng ta có thể tùy chỉnh đầu vào edittext!


0

Đây là cách tôi tạo bộ lọc cho trường Tên trong Chỉnh sửa văn bản. (Chữ cái đầu tiên là CAPS và chỉ cho phép một khoảng trống sau mỗi từ.

public void setNameFilter() {
    InputFilter filter = new InputFilter() {
        @RequiresApi(api = Build.VERSION_CODES.KITKAT)
        public CharSequence filter(CharSequence source, int start, int end,
                                   Spanned dest, int dstart, int dend) {
            for (int i = start; i < end; i++) {
                if (dend == 0) {
                    if (Character.isSpaceChar(source.charAt(i)) ||
                            !Character.isAlphabetic(source.charAt(i))) {
                        return Constants.Delimiter.BLANK;
                    } else {
                        return String.valueOf(source.charAt(i)).toUpperCase();
                    }
                } else if (Character.isSpaceChar(source.charAt(i)) &&
                        String.valueOf(dest).endsWith(Constants.Delimiter.ONE_SPACE)) {
                    return Constants.Delimiter.BLANK;
                } else if ((!Character.isSpaceChar(source.charAt(i)) &&
                        !Character.isAlphabetic(source.charAt(i)))) {
                    return Constants.Delimiter.BLANK;
                }
            }
            return null;
        }
    };
    editText.setFilters(new InputFilter[]{filter, new InputFilter.LengthFilter(Constants.Length.NAME_LENGTH)});
}

Hằng số.D006iter.BLANK không được xác định.
CoolMind

0

Tôi có câu trả lời tương tự trong Kotlin:

/**
 * Returns the filter of the editText'es CharSequence value when [filterType] is:
 * 1 -> letters; 2 -> letters and digits; 3 -> digits;
 * 4 -> digits and dots
 */
class InputFilterAlphanumeric(private val filterType: Int): InputFilter {
    override fun filter(source: CharSequence?, start: Int, end: Int, dest: Spanned?, dstart: Int, dend: Int): CharSequence {
        (source as? SpannableStringBuilder)?.let {sourceAsSpannableBuilder  ->
            for (i in (end - 1) downTo start) {
                val currentChar = source[i]
                when(filterType) {
                    1 -> {
                        if (!currentChar.isLetter() && !currentChar.isWhitespace()) {
                            sourceAsSpannableBuilder.delete(i, i + 1)
                        }
                    }
                    2 -> {
                        if (!currentChar.isLetterOrDigit() && !currentChar.isWhitespace()) {
                            sourceAsSpannableBuilder.delete(i, i + 1)
                        }
                    }
                    3 -> {
                        if (!currentChar.isDigit()) {
                            sourceAsSpannableBuilder.delete(i, i + 1)
                        }
                    }
                    4 -> {
                        if (!currentChar.isDigit() || !currentChar.toString().contains(".")) {
                            sourceAsSpannableBuilder.delete(i, i + 1)
                        }
                    }
                }
            }
            return source
        } ?: run {
            val filteredStringBuilder = StringBuilder()
            for (i in start until end) {
                val currentChar = source?.get(i)
                when(filterType) {
                    1 -> {
                        if (currentChar?.isLetter()!! || currentChar.isWhitespace()) {
                            filteredStringBuilder.append(currentChar)
                        }
                    }
                    2 -> {
                        if (currentChar?.isLetterOrDigit()!! || currentChar.isWhitespace()) {
                            filteredStringBuilder.append(currentChar)
                        }
                    }
                    3 -> {
                        if (currentChar?.isDigit()!!) {
                            filteredStringBuilder.append(currentChar)
                        }
                    }
                    4 -> {
                        if (currentChar?.isDigit()!! || currentChar.toString().contains(".")) {
                            filteredStringBuilder.append(currentChar)
                        }
                    }
                }
            }
            return filteredStringBuilder
        }
    }
}

và nhận lớp với chức năng Mở rộng:

fun EditText.filterByDataType(filterType: Int) {
    this.filters = arrayOf<InputFilter>(InputFilterAlphanumeric(filterType))
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.