Як зберегти позицію прокрутки RecyclerView за допомогою RecyclerView.State?


107

У мене є питання щодо Android RecyclerView.State .

Я використовую RecyclerView, як я можу використовувати його і пов'язувати його з RecyclerView.State?

Моя мета - зберегти позицію прокрутки RecyclerView .

Відповіді:


37

Як ви плануєте зберегти останню збережену позицію RecyclerView.State?

Ви завжди можете розраховувати на добрий стан економії. Розширення RecyclerViewта перезапис наSaveInstanceState () та onRestoreInstanceState():

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        LayoutManager layoutManager = getLayoutManager();
        if(layoutManager != null && layoutManager instanceof LinearLayoutManager){
            mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        }
        SavedState newState = new SavedState(superState);
        newState.mScrollPosition = mScrollPosition;
        return newState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if(state != null && state instanceof SavedState){
            mScrollPosition = ((SavedState) state).mScrollPosition;
            LayoutManager layoutManager = getLayoutManager();
            if(layoutManager != null){
              int count = layoutManager.getItemCount();
              if(mScrollPosition != RecyclerView.NO_POSITION && mScrollPosition < count){
                  layoutManager.scrollToPosition(mScrollPosition);
              }
            }
        }
    }

    static class SavedState extends android.view.View.BaseSavedState {
        public int mScrollPosition;
        SavedState(Parcel in) {
            super(in);
            mScrollPosition = in.readInt();
        }
        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(mScrollPosition);
        }
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

1
Тепер у мене є ще одне питання, як, ні, що може зробити RecyclerView.State?
陈文涛

4
це справді працює? Я спробував це, але я продовжую отримувати винятки з виконання, як це: MyRecyclerView $ SavedState не можна передавати на android.support.v7.widget.RecyclerView $ SavedState
Daivid

2
У стані екземпляра відновлення нам потрібно передати superState його суперкласу (перегляд recytcler) таким чином: super.onRestoreInstanceState (((стан (ScrollPositionSavingRecyclerView.SavedState)) .getSuperState ());
micnoy

5
Це не працює, якщо розширити RecyclerView, ви отримаєте BadParcelException.
EpicPandaForce

1
Нічого цього не потрібно: якщо ви використовуєте щось на зразок LinearLayoutManager / GridLayoutManager, "позиція прокрутки" вже автоматично зберігається для вас. (Це mAnchorPosition у LinearLayoutManager.SavedState). Якщо ви цього не бачите, значить, щось не так у тому, як ви повторно використовуєте дані.
Брайан Єнчо

229

Оновлення

Починаючи з recyclerview:1.2.0-alpha02випуску StateRestorationPolicyбуло введено. Це може бути кращим підходом до даної проблеми.

Ця тема висвітлюється в середній статті розробників для Android .

Крім того, @ rubén-viguera поділився більше деталями у відповіді нижче. https://stackoverflow.com/a/61609823/892500

Стара відповідь

Якщо ви використовуєте LinearLayoutManager, він постачається з попередньо вбудованим збереженням api linearLayoutManagerInstance.onSaveInstanceState () та відновленням api linearLayoutManagerInstance.onRestoreInstanceState (...)

За допомогою цього ви можете зберегти повернутий посилку у свою outState. наприклад,

outState.putParcelable("KeyForLayoutManagerState", linearLayoutManagerInstance.onSaveInstanceState());

та відновіть положення відновлення зі збереженим вами станом. наприклад,

Parcelable state = savedInstanceState.getParcelable("KeyForLayoutManagerState");
linearLayoutManagerInstance.onRestoreInstanceState(state);

Щоб завершити все, ваш остаточний код буде виглядати приблизно так

private static final String BUNDLE_RECYCLER_LAYOUT = "classname.recycler.layout";

/**
 * This is a method for Fragment. 
 * You can do the same in onCreate or onRestoreInstanceState
 */
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);

    if(savedInstanceState != null)
    {
        Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        recyclerView.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, recyclerView.getLayoutManager().onSaveInstanceState());
}

Редагувати: Ви також можете використовувати той самий apis з GridLayoutManager, оскільки це підклас LinearLayoutManager. Дякую @wegsehen за пропозицію.

Редагувати: Пам’ятайте, якщо ви також завантажуєте дані у фоновий потік, вам потрібно буде зателефонувати на onRestoreInstanceState у своєму методі onPostExecute / onLoadFinished, щоб позиція була відновлена ​​при зміні орієнтації, наприклад

@Override
protected void onPostExecute(ArrayList<Movie> movies) {
    mLoadingIndicator.setVisibility(View.INVISIBLE);
    if (movies != null) {
        showMoviePosterDataView();
        mDataAdapter.setMovies(movies);
      mRecyclerView.getLayoutManager().onRestoreInstanceState(mSavedRecyclerLayoutState);
        } else {
            showErrorMessage();
        }
    }

21
Це, очевидно, найкраща відповідь, чому це не прийнято? Зауважте, що будь-який LayoutManager має це не тільки LinearLayoutManager, але і GridLayoutManager, і якщо не його хибна реалізація.
Пол Войтащек

linearLayoutManager.onInstanceState () завжди повертає null -> перевірити вихідний код. На жаль, не працює.
luckyhandler

1
Чи RecyclerView не зберігає стан свого LayoutManager у власному OnSaveInstanceState? Дивлячись на v24 підтримки lib ...
androidguy

3
Це працює на один , RecyclerViewале не тоді , коли у вас є ViewPagerз RecycerViewна кожній сторінці.
Кріс Б

1
@Ivan, я думаю, він працює у фрагменті (щойно перевірений), але не в onCreateView, а після завантаження даних. Дивіться відповідь @Farmaker тут.
CoolMind

81

Магазин

lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();

Відновлення

((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);

і якщо це не працює, спробуйте

((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)

Покладіть магазин onPause() і відновітьonResume()


Це працює, але вам може знадобитися скинути lastFirstVisiblePositionдо 0після відновлення його. Цей випадок корисний, коли у вас є фрагмент / активність, що завантажує різні дані в одне RecyclerView, наприклад, програма провідника файлів.
blueware

Відмінно! Це задовольняє як повернення від детальної діяльності до списку, так і збереження стану при обертанні екрану
Javi

що робити, якщо мій утилізатор переглядає SwipeRefreshLayout?
Морозов

2
Чому перший варіант відновлення не працює, а другий буде?
lucidbrot

1
@MehranJalili здається, що цеRecyclerView
Влад

22

Я хотів зберегти позицію прокрутки Recycler View, коли відходив від діяльності зі списку, а потім натиснув кнопку "Назад", щоб повернутися назад. Багато варіантів рішень для цієї проблеми були або набагато складнішими, ніж потрібно, або не працювали для моєї конфігурації, тому я подумав, що поділюся своїм рішенням.

Спочатку збережіть стан свого примірника в onPause, як показали багато. Я думаю, тут варто підкреслити, що ця версія onSaveInstanceState є методом з класу RecyclerView.LayoutManager.

private LinearLayoutManager mLayoutManager;
Parcelable state;

        @Override
        public void onPause() {
            super.onPause();
            state = mLayoutManager.onSaveInstanceState();
        }

Ключовим фактором для правильної роботи є переконайтеся, що ви зателефонуєте наReRestoreInstanceState після того, як ви приєднаєте адаптер, як це було зазначено в інших потоках. Однак фактичний виклик методу набагато простіший, ніж багато хто вказав.

private void someMethod() {
    mVenueRecyclerView.setAdapter(mVenueAdapter);
    mLayoutManager.onRestoreInstanceState(state);
}

1
Це точне рішення. : D
Afjalur Rahman Rana

1
для мене onSaveInstanceState не називається, тому збереження стану в onPause здається кращим варіантом. Я також poping backstack повернутися до фрагменту.
filthy_wizard

для завантаження в державі. що я роблю в onViewCreate. це, здається, працює лише в режимі налагодження і використовує точки перерви? або якщо я додаю затримку і поміщую виконання у підпрограму.
filthy_wizard

21

I Встановлення змінних в onCreate (), збереження позиції прокрутки в onPause () і встановлення позиції прокрутки в onResume ()

public static int index = -1;
public static int top = -1;
LinearLayoutManager mLayoutManager;

@Override
public void onCreate(Bundle savedInstanceState)
{
    //Set Variables
    super.onCreate(savedInstanceState);
    cRecyclerView = ( RecyclerView )findViewById(R.id.conv_recycler);
    mLayoutManager = new LinearLayoutManager(this);
    cRecyclerView.setHasFixedSize(true);
    cRecyclerView.setLayoutManager(mLayoutManager);

}

@Override
public void onPause()
{
    super.onPause();
    //read current recyclerview position
    index = mLayoutManager.findFirstVisibleItemPosition();
    View v = cRecyclerView.getChildAt(0);
    top = (v == null) ? 0 : (v.getTop() - cRecyclerView.getPaddingTop());
}

@Override
public void onResume()
{
    super.onResume();
    //set recyclerview position
    if(index != -1)
    {
        mLayoutManager.scrollToPositionWithOffset( index, top);
    }
}

Залишайте зміщення предмета теж. Приємно. Дякую!
t0m


14

Усі менеджери макетів, що входять у бібліотеку підтримки, вже знають, як зберегти та відновити позицію прокрутки.

Позиція прокрутки відновлюється під час першого пропуску макета, що відбувається, коли виконуються наступні умови:

  • менеджер макетів додається до RecyclerView
  • до адаптера приєднано адаптер RecyclerView

Зазвичай ви встановлюєте диспетчер макетів у XML, тому все, що вам потрібно зробити зараз

  1. Завантажте адаптер з даними
  2. Потім приєднайте адаптер доRecyclerView

Ви можете це зробити в будь-який час, це не обмежується Fragment.onCreateViewабо Activity.onCreate.

Приклад: Переконайтеся, що адаптер підключається щоразу, коли дані оновлюються.

viewModel.liveData.observe(this) {
    // Load adapter.
    adapter.data = it

    if (list.adapter != adapter) {
        // Only set the adapter if we didn't do it already.
        list.adapter = adapter
    }
}

Збережена позиція прокрутки обчислюється з таких даних:

  • Положення адаптера першого елемента на екрані
  • Зсув пікселя вгорі елемента вгорі списку (для вертикального макета)

Тому ви можете очікувати незначної невідповідності, наприклад, якщо ваші предмети мають різні розміри в портретній та пейзажній орієнтації.


RecyclerView/ ScrollView/ Що потрібно мати android:idдля своєї держави , щоб врятуватися.


Це було для мене. Я налаштовував адаптер спочатку, а потім оновлював його дані, коли вони були доступні. Як було пояснено вище, залишаючи його, поки наявні дані не зроблять його автоматично прокручуванням назад, де він був.
NeilS

Дякую @Eugen Чи є документація щодо збереження та відновлення позиції прокрутки LayoutManager?
Адам Гурвіц,

@AdamHurwitz Яку документацію ви очікували? Те, що я описав, - це деталізація реалізації LinearLayoutManager, тому прочитайте її вихідний код.
Євген Печанець,

1
Ось посилання на LinearLayoutManager.SavedState: cs.android.com/androidx/platform/frameworks/support/+/…
Євген

Дякую, @EugenPechanec. Це працювало для мене на обертання пристроїв. Я також використовую подання навігації внизу, і коли я переключаюсь на іншу нижню вкладку і повертаюся назад, список починається з початку знову. Будь-які ідеї, чому це так?
schv09

4

Ось мій підхід до збереження їх та підтримання стану рециркулятора в фрагменті. Спочатку додайте зміни конфігурації у батьківській діяльності, як показано нижче.

android:configChanges="orientation|screenSize"

Потім у своєму фрагменті оголосіть методи цього примірника

private  Bundle mBundleRecyclerViewState;
private Parcelable mListState = null;
private LinearLayoutManager mLinearLayoutManager;

Додайте код нижче у своєму методі @onPause

@Override
public void onPause() {
    super.onPause();
    //This used to store the state of recycler view
    mBundleRecyclerViewState = new Bundle();
    mListState =mTrailersRecyclerView.getLayoutManager().onSaveInstanceState();
    mBundleRecyclerViewState.putParcelable(getResources().getString(R.string.recycler_scroll_position_key), mListState);
}

Додайте метод onConfigartionChanged, як показано нижче.

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    //When orientation is changed then grid column count is also changed so get every time
    Log.e(TAG, "onConfigurationChanged: " );
    if (mBundleRecyclerViewState != null) {
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                mListState = mBundleRecyclerViewState.getParcelable(getResources().getString(R.string.recycler_scroll_position_key));
                mTrailersRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);

            }
        }, 50);
    }
    mTrailersRecyclerView.setLayoutManager(mLinearLayoutManager);
}

Цей підхід вирішить вашу проблему. якщо у вас різний диспетчер макетів, просто замініть верхній диспетчер верстки.


Можливо, вам не потрібен обробник для збереження даних. Способів життєвого циклу буде достатньо для економії / завантаження.
Махі

@sayaMahi Чи можете ви описати належним чином. Я також бачив, що onConfigurationChanged заважає застосовувати метод знищення. тому це не правильне рішення.
Pawan kumar sharma

3

Так я відновляю позицію RecyclerView за допомогою GridLayoutManager після обертання, коли потрібно перезавантажити дані з Інтернету за допомогою AsyncTaskLoader.

Зробіть глобальну змінну Parcelable та GridLayoutManager та статичну кінцеву рядок:

private Parcelable savedRecyclerLayoutState;
private GridLayoutManager mGridLayoutManager;
private static final String BUNDLE_RECYCLER_LAYOUT = "recycler_layout";

Збережіть стан gridLayoutManager в onSaveInstance ()

 @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, 
            mGridLayoutManager.onSaveInstanceState());
        }

Відновити в onRestoreInstanceState

@Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);

        //restore recycler view at same position
        if (savedInstanceState != null) {
            savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        }
    }

Потім, коли завантажувач отримує дані з Інтернету, ви відновлюєте позицію reciklierview в режимі onLoadFinished ()

if(savedRecyclerLayoutState!=null){
                mGridLayoutManager.onRestoreInstanceState(savedRecyclerLayoutState);
            }

.. звичайно вам доведеться інстанціювати gridLayoutManager всередині onCreate. Ура


3

У мене була така ж вимога, але рішення тут не дуже перешкодили мені через джерело даних для recilerView.

Я витягував стан LinearLayoutManager RecyclerViews в режимі onPause ()

private Parcelable state;

Public void onPause() {
    state = mLinearLayoutManager.onSaveInstanceState();
}

Парцелябел stateзберігається в onSaveInstanceState(Bundle outState), і знову вилучається в onCreate(Bundle savedInstanceState), колиsavedInstanceState != null .

Однак адаптер recilerView заповнюється та оновлюється об’єктом ViewModel LiveData, який повертається викликом DAO до бази даних кімнати.

mViewModel.getAllDataToDisplay(mSelectedData).observe(this, new Observer<List<String>>() {
    @Override
    public void onChanged(@Nullable final List<String> displayStrings) {
        mAdapter.swapData(displayStrings);
        mLinearLayoutManager.onRestoreInstanceState(state);
    }
});

Я врешті-решт виявив, що відновлення стану екземпляра безпосередньо після встановлення даних у адаптері буде зберігати стан прокрутки через обертання.

Я підозрюю, що це теж тому, що LinearLayoutManager стан, який я відновив, був перезаписаний, коли дані поверталися при виклику бази даних, або відновлений стан був безглуздим проти 'порожнього' LinearLayoutManager.

Якщо дані адаптера доступні безпосередньо (тобто не є суміжним під час виклику бази даних), відновлення стану екземпляра в LinearLayoutManager може бути виконано після встановлення адаптера на recilerView.

Різниця між двома сценаріями тримала мене на віки.


3

На Android API Level 28 я просто гарантую, що я налаштував свій LinearLayoutManagerі RecyclerView.Adapterв своєму Fragment#onCreateViewметоді, і все, що тільки працював ™ ️. Мені не потрібно було нічого робити onSaveInstanceStateчи onRestoreInstanceStateпрацювати.

Відповідь Євгена Печанека пояснює, чому це працює.


2

Починаючи з версії 1.2.0-альфа02 бібліотеки для androidx recilerView, тепер ним керується автоматично. Просто додайте його:

implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"

І використовуйте:

adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY

Перелік StateRestorationPolicy має 3 варіанти:

  • ALLOW - стан за замовчуванням, який відновлює стан RecyclerView негайно, у наступному пропуску макета
  • PREVENT_WHEN_EMPTY - відновлює стан RecyclerView лише тоді, коли адаптер не порожній (adapter.getItemCount ()> 0). Якщо ваші дані завантажуються асинхронізованими, RecyclerView чекає, поки дані будуть завантажені, і лише тоді стан відновиться. Якщо у вас є елементи за замовчуванням, такі як заголовки або завантажувачі індикаторів прогресу, як частина адаптера, тоді вам слід скористатись опцією PREVENT , якщо тільки елементи за замовчуванням не додані за допомогою MergeAdapter . MergeAdapter очікує, що всі його адаптери будуть готові, і лише тоді він відновлює стан.
  • PREVENT - усі відновлення стану відкладені, поки ви не встановите ALLOW або PREVENT_WHEN_EMPTY.

Зауважте, що під час цієї відповіді бібліотека recilerView все ще знаходиться в альфа03, але альфа-фаза не підходить для виробничих цілей .


Стільки перевтоми врятували. Інакше нам довелося зберегти список кліків списку, відновити елементи, запобігти перемальовуванню в onCreateView, застосувати scrollToPosition (або зробити так, як інші відповіді), і все ж користувач міг помітити різницю після повернення
Rahul Kahale

Крім того, якщо я хочу зберегти позицію RecyclerView після навігації через додаток ... чи є якийсь інструмент для цього?
StayCool

@RubenViguera Чому альфа не призначена для виробничих цілей? Я маю на увазі, чи справді це погано?
StayCool

1

Для мене проблема полягала в тому, що я встановлював новий менеджер верстки щоразу, коли міняв адаптер, втрачаючи позицію прокрутки черга на recilerView.


1

Я просто хотів би поділитися недавньою проблемою, з якою я стикаюся з RecyclerView. Я сподіваюся, що кожен, хто відчуває ту саму проблему, піде на користь.

Моя вимога проекту: Отже, у мене є RecyclerView, який перераховує деякі елементи, які можна натиснути в моїй Основній діяльності (Activity-A). При натисканні на пункт з'являється нова активність із деталями елемента (активність-B).

Я реалізовувався в Маніфесті файлі той факт, що Activity-A є батьківською мовою Activity-B, таким чином, у мене є кнопка "назад" чи "домашня сторінка" на панелі ActionBar активності-B

Проблема: Кожного разу, коли я натискав кнопку "Назад" або "Головна" в панелі "Діяльність-Б", активність-А переходить у повний життєвий цикл діяльності, починаючи з onCreate ()

Навіть незважаючи на те, що я реалізував OnSaveInstanceState (), що зберігає список адаптера RecyclerView, коли Activity-A починає його життєвий цикл, метод saveInstanceState завжди є нульовим у методі onCreate ().

Подальше копаючись в Інтернеті, я зіткнувся з тією ж проблемою, але людина помітила, що кнопка "Назад" або "Головна" під пристроєм Anroid (кнопка "Назад / Головна") Activity-A не переходить у життєвий цикл діяльності.

Рішення:

  1. Я видалив тег для батьківської активності в маніфесті для діяльності-B
  2. Я ввімкнув кнопку "Додому" або "Назад" на "Активність-Б"

    Під методом onCreate () додайте цей рядок supportActionBar? .SetDisplayHomeAsUpEnabled (true)

  3. У методі overide fun onOptionsItemSelected () я перевірив на предмет item.ItemId, на який елемент натискається на основі ідентифікатора. Ідентифікатор кнопки "назад" є

    android.R.id.home

    Потім реалізуйте виклик функції закінчення () всередині будинку android.R.id.home

    Це припинить Діяльність-Б і приведе Acitivy-A, не проходячи весь життєвий цикл.

На мою вимогу це найкраще рішення поки що.

     override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_show_project)

    supportActionBar?.title = projectName
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

}


 override fun onOptionsItemSelected(item: MenuItem?): Boolean {

    when(item?.itemId){

        android.R.id.home -> {
            finish()
        }
    }

    return super.onOptionsItemSelected(item)
}

1

У мене була така ж проблема з незначною різницею, я використовував Databinding , і я встановлював переробник recilerView та layoutManager в методах зв'язування.

Проблема полягала в тому, що прив'язка даних відбувалася після onResume, тому вона скидала мій макет-менеджер після onResume, і це означає, що він прокручується назад до початкового місця кожного разу. Тож коли я зупинив налаштування layoutManager в методах адаптера даних, це знову працює добре.


як ти цього досягнув?
Кедар Сукеркар

0

У моєму випадку я встановлював RecyclerView layoutManagerі в XML, і в onViewCreated. Видалення завдання onViewCreatedзафіксовано.

with(_binding.list) {
//  layoutManager =  LinearLayoutManager(context)
    adapter = MyAdapter().apply {
        listViewModel.data.observe(viewLifecycleOwner,
            Observer {
                it?.let { setItems(it) }
            })
    }
}

-1

Activity.java:

public RecyclerView.LayoutManager mLayoutManager;

Parcelable state;

    mLayoutManager = new LinearLayoutManager(this);


// Inside `onCreate()` lifecycle method, put the below code :

    if(state != null) {
    mLayoutManager.onRestoreInstanceState(state);

    } 

    @Override
    protected void onResume() {
        super.onResume();

        if (state != null) {
            mLayoutManager.onRestoreInstanceState(state);
        }
    }   

   @Override
    protected void onPause() {
        super.onPause();

        state = mLayoutManager.onSaveInstanceState();

    }   

Чому я використовую OnSaveInstanceState()в onPause()засіб, в той час перехід на інший вид діяльності OnPause буде called.It збереже цю позицію прокрутки і відновити становище , коли повертаючись з іншої діяльності.


2
Публічне статичне поле не добре. Глобальні дані та одинаки - це не добре.
weston

@weston Я зрозумів зараз. Я змінив збереження держави на onpause, щоб видалити глобальний.
Стів
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.