Bagaimana cara menangkap acara "virtual keyboard show / hide" di Android?


Jawaban:


69

Catatan

Solusi ini tidak akan berfungsi untuk keyboard lunak dan onConfigurationChangedtidak akan dipanggil untuk keyboard lunak (virtual).


Anda harus menangani sendiri perubahan konfigurasi.

http://developer.android.com/guide/topics/resources/runtime-changes.html#MenanganiTheChange

Sampel:

// from the link above
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);


    // Checks whether a hardware keyboard is available
    if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
        Toast.makeText(this, "keyboard visible", Toast.LENGTH_SHORT).show();
    } else if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "keyboard hidden", Toast.LENGTH_SHORT).show();
    }
}

Kemudian cukup ubah visibilitas beberapa tampilan, perbarui bidang, dan ubah file tata letak Anda.


4
@shiami try newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO~ Chris
cimnine

3
Ini hanya berfungsi jika Anda telah mendaftarkan aktivitas untuk mendengarkan perubahan konfigurasi yang Anda inginkan di AndroidManifest.
Raul Agrait

65
perbarui jawaban Anda dan katakan bahwa itu tidak berfungsi untuk keyboard lunak. Saya menghabiskan setengah hari untuk mencoba kode Anda. Dan kemudian melihat komentar ini.
Shirish Herwade

17
Ini tidak berfungsi untuk keyboard "virtual" yang merupakan pertanyaan awal.
brummfondel

18
Nah, pertanyaannya adalah tentang KEYBOARD LEMBUT, mengapa jawaban yang diterima tentang keyboard perangkat keras? -1!
Denys Vitali

56

Ini mungkin bukan solusi yang paling efektif. Tetapi ini bekerja untuk saya setiap kali ... Saya memanggil fungsi ini di mana pun saya perlu mendengarkan softKeyboard.

boolean isOpened = false;

public void setListenerToRootView() {
    final View activityRootView = getWindow().getDecorView().findViewById(android.R.id.content);
    activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {

            int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();
            if (heightDiff > 100) { // 99% of the time the height diff will be due to a keyboard.
                Toast.makeText(getApplicationContext(), "Gotcha!!! softKeyboardup", 0).show();

                if (isOpened == false) {
                    //Do two things, make the view top visible and the editText smaller
                }
                isOpened = true;
            } else if (isOpened == true) {
                Toast.makeText(getApplicationContext(), "softkeyborad Down!!!", 0).show();
                isOpened = false;
            }
        }
    });
}

Catatan: Pendekatan ini akan menyebabkan masalah jika pengguna menggunakan keyboard mengambang.


1
addOnGlobalLayoutListener?
coolcool1994

7
Baunya seperti kebocoran memori. Anda menambahkan pendengar ke objek global, yang akan mempertahankan Anda dan tidak pernah membiarkan Anda pergi.
flexicious.com

9
Yang ini juga tidak akan berfungsi untuk Aktivitas yang diatur dengan android:windowSoftInputMode="adjustPan", atau adjustResizedengan jendela layar penuh, karena tata letak tidak pernah diubah ukurannya.
Ionoclast Brigham

1
Ini hanya bekerja dengan adjustResize. Untuk AdjustPan, heightDiff tidak pernah berubah.
alexhilton

2
kenapa kamu membandingkan boolean?
Xerus

37

Jika Anda ingin menangani show / hide jendela keyboard IMM (virtual) dari Aktivitas Anda, Anda harus mensubklasifikasikan tata letak dan mengganti metode onMesure (sehingga Anda dapat menentukan lebar yang diukur dan tinggi yang diukur dari tata letak Anda). Setelah itu tetapkan tata letak subklas sebagai tampilan utama untuk Aktivitas Anda dengan setContentView (). Sekarang Anda akan dapat menangani acara IMM show / hide window. Jika ini terdengar rumit, sebenarnya tidak terlalu. Ini kodenya:

main.xml

   <?xml version="1.0" encoding="utf-8"?>
   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="horizontal" >
        <EditText
             android:id="@+id/SearchText" 
             android:text="" 
             android:inputType="text"
             android:layout_width="fill_parent"
             android:layout_height="34dip"
             android:singleLine="True"
             />
        <Button
             android:id="@+id/Search" 
             android:layout_width="60dip"
             android:layout_height="34dip"
             android:gravity = "center"
             />
    </LinearLayout>

Sekarang di dalam Activity Anda, deklarasikan subclass untuk layout Anda (main.xml)

    public class MainSearchLayout extends LinearLayout {

    public MainSearchLayout(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.main, this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d("Search Layout", "Handling Keyboard Window shown");

        final int proposedheight = MeasureSpec.getSize(heightMeasureSpec);
        final int actualHeight = getHeight();

        if (actualHeight > proposedheight){
            // Keyboard is shown

        } else {
            // Keyboard is hidden
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

Anda dapat melihat dari kode bahwa kami mengembang tata letak untuk Aktivitas kami di konstruktor subkelas

inflater.inflate(R.layout.main, this);

Dan sekarang hanya mengatur tampilan konten tata letak subclass untuk Aktivitas kami.

public class MainActivity extends Activity {

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        MainSearchLayout searchLayout = new MainSearchLayout(this, null);

        setContentView(searchLayout);
    }

    // rest of the Activity code and subclassed layout...

}

3
Saya perlu menyelidiki lebih lanjut tetapi saya ragu apakah ini akan berhasil dalam kasus saya untuk dialog kecil pada perangkat layar besar yang ukuran tata letak tidak akan terpengaruh oleh kehadiran keyboard.
PJL

4
Ini tidak berfungsi untuk android: windowSoftInputMode = "AdjustPan". Saya ingin agar layar saya tidak menyusut setelah keyboard lunak muncul. Bisakah Anda memberi tahu perbaikan apa pun agar itu berfungsi bahkan untuk AdjustPan
Shirish Herwade

Ini tidak berfungsi, ia selalu menuju bagian yang lain di sini jika (actualHeight> usulanheight) {// Keyboard ditampilkan} else {// Keyboard disembunyikan}
Aamirkhan

Anda juga dapat menggunakan Tampilan Kustom dengan ide yang sama, mengikuti contoh gist.github.com/juliomarcos/8ca307cd7eca607c8547
Julio Rodrigues

1
Tidak akan berfungsi untuk Aktivitas yang diatur dengan android:windowSoftInputMode="adjustPan", atau adjustResizedengan jendela layar penuh, karena tata letak tidak pernah diubah ukurannya.
Ionoclast Brigham

35

Saya melakukan ini:

Tambahkan OnKeyboardVisibilityListenerantarmuka.

public interface OnKeyboardVisibilityListener {
    void onVisibilityChanged(boolean visible);
}

HomeActivity.java :

public class HomeActivity extends Activity implements OnKeyboardVisibilityListener {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sign_up);
    // Other stuff...
    setKeyboardVisibilityListener(this);
}

private void setKeyboardVisibilityListener(final OnKeyboardVisibilityListener onKeyboardVisibilityListener) {
    final View parentView = ((ViewGroup) findViewById(android.R.id.content)).getChildAt(0);
    parentView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

        private boolean alreadyOpen;
        private final int defaultKeyboardHeightDP = 100;
        private final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0);
        private final Rect rect = new Rect();

        @Override
        public void onGlobalLayout() {
            int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics());
            parentView.getWindowVisibleDisplayFrame(rect);
            int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top);
            boolean isShown = heightDiff >= estimatedKeyboardHeight;

            if (isShown == alreadyOpen) {
                Log.i("Keyboard state", "Ignoring global layout change...");
                return;
            }
            alreadyOpen = isShown;
            onKeyboardVisibilityListener.onVisibilityChanged(isShown);
        }
    });
}


@Override
public void onVisibilityChanged(boolean visible) {
    Toast.makeText(HomeActivity.this, visible ? "Keyboard is active" : "Keyboard is Inactive", Toast.LENGTH_SHORT).show();
  }
}

Semoga ini bisa membantu Anda.


2
Terima kasih Hiren. Ini adalah solusi sempurna +1
Harin Kaklotar

1
Terima kasih, berhasil untuk saya! Jika Anda hanya ingin menyesuaikan RecyclerView Anda, lihat solusinya di sini: stackoverflow.com/a/43204258/373106
David Papirov

1
Implementasi yang dapat digunakan kembali dengan sempurna, bekerja dalam Activity atau Fragment, terima kasih
Pelanes

1
ty sangat bagus.
ZaoTaoBao

@ Davidvidapi, Anda menempelkan tautan ke RecyclerView, tetapi tidak disebutkan di sini.
CoolMind

22

Berdasarkan Kode dari Nebojsa Tomcic, saya telah mengembangkan RelativeLayout-Subclass berikut:

import java.util.ArrayList;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;

public class KeyboardDetectorRelativeLayout extends RelativeLayout {

    public interface IKeyboardChanged {
        void onKeyboardShown();
        void onKeyboardHidden();
    }

    private ArrayList<IKeyboardChanged> keyboardListener = new ArrayList<IKeyboardChanged>();

    public KeyboardDetectorRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public KeyboardDetectorRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public KeyboardDetectorRelativeLayout(Context context) {
        super(context);
    }

    public void addKeyboardStateChangedListener(IKeyboardChanged listener) {
        keyboardListener.add(listener);
    }

    public void removeKeyboardStateChangedListener(IKeyboardChanged listener) {
        keyboardListener.remove(listener);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int proposedheight = MeasureSpec.getSize(heightMeasureSpec);
        final int actualHeight = getHeight();

        if (actualHeight > proposedheight) {
            notifyKeyboardShown();
        } else if (actualHeight < proposedheight) {
            notifyKeyboardHidden();
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    private void notifyKeyboardHidden() {
        for (IKeyboardChanged listener : keyboardListener) {
            listener.onKeyboardHidden();
        }
    }

    private void notifyKeyboardShown() {
        for (IKeyboardChanged listener : keyboardListener) {
            listener.onKeyboardShown();
        }
    }

}

Ini berfungsi dengan baik ... Tandai, bahwa solusi ini hanya akan bekerja ketika Mode Input Lunak dari Aktivitas Anda diatur ke "WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE"


3
Ini tidak berfungsi untuk android: windowSoftInputMode = "AdjustPan". Saya ingin agar layar saya tidak menyusut setelah keyboard lunak muncul. Bisakah Anda memberi tahu perbaikan apa pun agar itu berfungsi bahkan untuk AdjustPan
Shirish Herwade

1
Yang ini juga tidak akan berfungsi untuk Aktivitas yang diatur dengan android:windowSoftInputMode="adjustPan", atau adjustResizedengan jendela layar penuh, karena tata letak tidak pernah diubah ukurannya.
Ionoclast Brigham

itu triger beberapa kali.
zionpi

22

Seperti jawaban @ amalBit, daftarkan pendengar ke tata letak global dan hitung perbedaan bagian bawah yang terlihat dectorView dan bagian bawah yang diusulkan, jika perbedaannya lebih besar dari nilai tertentu (tebak tinggi IME), kami pikir IME naik:

    final EditText edit = (EditText) findViewById(R.id.edittext);
    edit.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            if (keyboardShown(edit.getRootView())) {
                Log.d("keyboard", "keyboard UP");
            } else {
                Log.d("keyboard", "keyboard Down");
            }
        }
    });

private boolean keyboardShown(View rootView) {

    final int softKeyboardHeight = 100;
    Rect r = new Rect();
    rootView.getWindowVisibleDisplayFrame(r);
    DisplayMetrics dm = rootView.getResources().getDisplayMetrics();
    int heightDiff = rootView.getBottom() - r.bottom;
    return heightDiff > softKeyboardHeight * dm.density;
}

ambang ketinggian 100 adalah tinggi minimum IME yang dapat ditebak.

Ini berfungsi untuk AdjustPan dan AdjustResize.


2
Saya baru saja akan menarik rambut saya !! Anda menyelamatkan rambut saya;)
Vijay Singh Chouhan

1
Ini satu-satunya jawaban yang bagus di sini, ini berfungsi pada soft keyboard sempurna, terima kasih
Z3nk

12

Solusi Nebojsa hampir berhasil untuk saya. Ketika saya mengklik di dalam EditText multi-baris itu tahu keyboard ditampilkan, tetapi ketika saya mulai mengetik di dalam EditText, aktualHeight dan usulanHeight masih sama sehingga tidak tahu bahwa keyboard mereka masih ditampilkan. Saya membuat sedikit modifikasi untuk menyimpan ketinggian maksimal dan berfungsi dengan baik. Ini subclass yang direvisi:

public class CheckinLayout extends RelativeLayout {

    private int largestHeight;

    public CheckinLayout(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.checkin, this);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int proposedheight = MeasureSpec.getSize(heightMeasureSpec);
        largestHeight = Math.max(largestHeight, getHeight());

        if (largestHeight > proposedheight)
            // Keyboard is shown
        else
            // Keyboard is hidden

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

10

Tidak yakin ada yang memposting ini. Menemukan solusi ini mudah digunakan! . Kelas SoftKeyboard ada di gist.github.com . Tetapi saat keyboard popup / sembunyikan event callback, kami membutuhkan penangan untuk melakukan hal-hal dengan benar di UI:

/*
Somewhere else in your code
*/
RelativeLayout mainLayout = findViewById(R.layout.main_layout); // You must use your root layout
InputMethodManager im = (InputMethodManager) getSystemService(Service.INPUT_METHOD_SERVICE);

/*
Instantiate and pass a callback
*/
SoftKeyboard softKeyboard;
softKeyboard = new SoftKeyboard(mainLayout, im);
softKeyboard.setSoftKeyboardCallback(new SoftKeyboard.SoftKeyboardChanged()
{

    @Override
    public void onSoftKeyboardHide() 
    {
        // Code here
        new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    // Code here will run in UI thread
                    ...
                }
            });
    }

    @Override
    public void onSoftKeyboardShow() 
    {
        // Code here
        new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    // Code here will run in UI thread
                    ...
                }
            });

    }   
});

di sini adalah Git untuk mendapatkan SoftkeyBoard " gist.github.com/felHR85/… "
douarbou

9

Saya mengatasi ini dengan mengesampingkan onKeyPreIme (int keyCode, acara KeyEvent) di EditText kustom saya.

@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
        //keyboard will be hidden
    }
}

Bagaimana cara menggunakannya dalam Fragmen atau Aktivitas? @Qbait
Maulik Dodia

Tidak berfungsi, itu bisa dipanggil hanya ketika saya meninggalkan halaman dalam kasus saya.
DysaniazzZ

ini adalah metode dari EditText, lihat jawaban ini: stackoverflow.com/a/5993196/2093236
Dmide

4

Saya memiliki semacam peretasan untuk melakukan ini. Meskipun ada tampaknya tidak menjadi cara untuk mendeteksi ketika keyboard lunak telah ditampilkan atau disembunyikan, Anda dapat sebenarnya mendeteksi ketika sekitar yang akan ditampilkan atau disembunyikan dengan menetapkanOnFocusChangeListener pada EditTextyang Anda sedang mendengarkan.

EditText et = (EditText) findViewById(R.id.et);
et.setOnFocusChangeListener(new View.OnFocusChangeListener()
    {
        @Override
        public void onFocusChange(View view, boolean hasFocus)
        {
            //hasFocus tells us whether soft keyboard is about to show
        }
    });

CATATAN: Satu hal yang perlu diperhatikan dengan peretasan ini adalah bahwa panggilan balik ini dipecat segera ketika EditTextkeuntungan atau kehilangan fokus. Ini akan benar-benar menyala tepat sebelum keyboard lunak muncul atau bersembunyi. Cara terbaik yang saya temukan untuk melakukan sesuatu setelah keyboard menunjukkan atau menyembunyikannya adalah dengan menggunakan Handlerdan menunda sesuatu ~ 400ms, seperti:

EditText et = (EditText) findViewById(R.id.et);
et.setOnFocusChangeListener(new View.OnFocusChangeListener()
    {
        @Override
        public void onFocusChange(View view, boolean hasFocus)
        {
            new Handler().postDelayed(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        //do work here
                    }
                }, 400);
        }
    });

1
Kalau tidak, itu tidak akan berhasil. OnFocusChangeListenerhanya memberi tahu apakah EditTextmemiliki fokus setelah keadaan berubah. Tapi yang IMEmungkin disembunyikan ketika EditTextsudah fokus, bagaimana mendeteksi kasus ini?
DysaniazzZ


2

Saya telah memecahkan masalah pada textview baris tunggal kembali coding.

package com.helpingdoc;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;

public class MainSearchLayout extends LinearLayout {
    int hieght = 0;
    public MainSearchLayout(Context context, AttributeSet attributeSet) {

        super(context, attributeSet);
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.main, this);


    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d("Search Layout", "Handling Keyboard Window shown");
       if(getHeight()>hieght){
           hieght = getHeight();
       }
        final int proposedheight = MeasureSpec.getSize(heightMeasureSpec);
        final int actualHeight = getHeight();
        System.out.println("....hieght = "+ hieght);
        System.out.println("....actualhieght = "+ actualHeight);
        System.out.println("....proposedheight = "+ proposedheight);
        if (actualHeight > proposedheight){
            // Keyboard is shown


        } else if(actualHeight<proposedheight){
            // Keyboard is hidden

        }

        if(proposedheight == hieght){
             // Keyboard is hidden
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

2
Ini tidak berfungsi untuk android: windowSoftInputMode = "AdjustPan". Saya ingin agar layar saya tidak menyusut setelah keyboard lunak muncul. Bisakah Anda memberi tahu perbaikan apa pun agar itu berfungsi bahkan untuk AdjustPan
Shirish Herwade

Ketika fungsi sembunyikan / tampilkan maka metode pendengar ini memanggil dua kali atau tiga kali. Saya tidak tahu persis apa masalahnya.
Jagveer Singh Rajput

2

Anda juga dapat memeriksa lapisan bawah anak DecorView pertama. Ini akan diatur ke nilai non-nol saat keyboard ditampilkan.

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    View view = getRootView();
    if (view != null && (view = ((ViewGroup) view).getChildAt(0)) != null) {
        setKeyboardVisible(view.getPaddingBottom() > 0);
    }
    super.onLayout(changed, left, top, right, bottom);
}

1

Sembunyikan | Tampilkan acara untuk keyboard dapat didengarkan melalui peretasan sederhana di OnGlobalLayoutListener:

 final View activityRootView = findViewById(R.id.top_root);
        activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            public void onGlobalLayout() {
                int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();

                if (heightDiff > 100) {
                    // keyboard is up
                } else {
                    // keyboard is down
                }
            }
        });

Di sini activityRootView adalah tampilan root Aktivitas Anda.


heightDiff saya 160 di awal dan 742 dengan kbd, jadi saya harus memperkenalkan dan mengatur initialHeightDiff di awal
djdance

0

menggunakan viewTreeObserver untuk dengan mudah mendapatkan acara keyboard.

layout_parent.viewTreeObserver.addOnGlobalLayoutListener {
            val r = Rect()
            layout_parent.getWindowVisibleDisplayFrame(r)
            if (layout_parent.rootView.height - (r.bottom - r.top) > 100) { // if more than 100 pixels, its probably a keyboard...
                Log.e("TAG:", "keyboard open")
            } else {
                Log.e("TAG:", "keyboard close")
            }
        }

** layout_parent adalah tampilan Andaedit_text.parent


-2

Jawaban Nebojsa Tomcic tidak membantu saya. Saya miliki RelativeLayoutdengan TextViewdan AutoCompleteTextViewdi dalamnya. Saya perlu menggulir TextViewke bawah ketika keyboard ditampilkan dan ketika itu disembunyikan. Untuk mencapai ini saya mengesampingkan onLayoutmetode dan itu berfungsi dengan baik untuk saya.

public class ExtendedLayout extends RelativeLayout
{
    public ExtendedLayout(Context context, AttributeSet attributeSet)
    {
        super(context, attributeSet);
        LayoutInflater inflater = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.main, this);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        super.onLayout(changed, l, t, r, b);

        if (changed)
        {
            int scrollEnd = (textView.getLineCount() - textView.getHeight() /
                textView.getLineHeight()) * textView.getLineHeight();
            textView.scrollTo(0, scrollEnd);
        }
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.