简体   繁体   中英

Android RecyclerView items shuffles on scroll

Items/cells of my multiple view type RecyclerView Adapter get shuffles on the scroll. I go through all below possible solutions but none of them working.

1.

@Override
public long getItemId(int position) {
   return mDataset.get(position).hashCode();
   or
   return mDataset.get(position).getBaseFormElementId();
}

setHasStableIds(true);
  1. Increased the cache of RecyclerView

Please share if anyone has any solution.

My Adapter Class

public class FormAdapter extends RecyclerView.Adapter<BaseViewHolder>  {

private Context mContext;
private List<BaseFormElement> mDataset;


public FormAdapter(Context context) {
    mContext = context;
    mDataset = new ArrayList<>();
    setHasStableIds(true);
 }



public List<BaseFormElement> getDataset() {
    return mDataset;
}


public OnFormElementValueChangedListener getValueChangeListener() {
    return mListener;
}


@Override
public int getItemCount() {
    return mDataset.size();
}


@Override
public int getItemViewType(int position) {
    return mDataset.get(position).getType();
}

@Override
public long getItemId(int position) {
    //return super.getItemId(position);

    return mDataset.get(position).getBaseFormElementId();
}


@NonNull
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    // get layout based on header or element type
    LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    View v;
    switch (viewType) {
        case BaseFormElement.TYPE_HEADER:
            v = inflater.inflate(R.layout.form_element_header, parent, false);
            return new FormElementHeader(v);
        case BaseFormElement.TYPE_EDIT_TEXT:
            v = inflater.inflate(R.layout.form_element, parent, false);
            return new FormElementEditTextViewHolder(v, new FormItemEditTextListener(this));
        case BaseFormElement.TYPE_PICKER_DATE:
            v = inflater.inflate(R.layout.form_element, parent, false);
            return new FormElementPickerDateViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_PICKER_TIME:
            v = inflater.inflate(R.layout.form_element, parent, false);
            return new FormElementPickerTimeViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_PICKER_SINGLE:
            v = inflater.inflate(R.layout.form_element, parent, false);
            return new FormElementPickerSingleViewHolder(v, mContext, this, new FormItemEditTextListener(this));
        case BaseFormElement.TYPE_PICKER_MULTI:
            v = inflater.inflate(R.layout.formelementlabel, parent, false);
            return new FormElementPickerMultiViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_IMAGE_REMARKS:
            v = inflater.inflate(R.layout.formelementlabel, parent, false);
            return new FormElementImageWithRemarksViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_SWITCH:
            v = inflater.inflate(R.layout.form_element_switch, parent, false);
            return new FormElementSwitchViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_SEGMENT:
            v = inflater.inflate(R.layout.form_element_switch, parent, false);
            return new FormElementSwitchViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_LABEL:
            v = inflater.inflate(R.layout.formelementlabel, parent, false);
            return new FormElementLabelViewHolder(v, clicklistner);
        case BaseFormElement.TYPE_IMAGE:
            v = inflater.inflate(R.layout.formelementlabel, parent, false);
            return new FromElementImageViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_VIDEO:
            v = inflater.inflate(R.layout.formelementlabel, parent, false);
            return new FormElementVideoViewHolder(v, mContext, this);
        case BaseFormElement.DIALOG_LIST:
            v = inflater.inflate(R.layout.form_element_mmv, parent, false);
            return new FormElementDialogListViewHolder(v, mContext, this);
        case BaseFormElement.TYPE_SLIDER:
            v = inflater.inflate(R.layout.formelementlabel, parent, false);
            return new FormElementSliderViewHolder(v, mContext, this);
        default:
            v = inflater.inflate(R.layout.form_element, parent, false);
            return new FormElementEditTextViewHolder(v, new FormItemEditTextListener(this));
    }
}


@Override
public void onBindViewHolder(BaseViewHolder holder, final int position) {
    // gets current object
    BaseFormElement currentObject = mDataset.get(position);
    holder.bind(position, currentObject, mContext);
}

}

I have worked on this type of RecyclerView and here is the DynamicAdapter class used with multiple view-type . I hope you get some clarity on how to use it.

    public class DynamicAdapter extends RecyclerView.Adapter <BaseViewHolder> {

  private static final String TAG = "DynamicAdapter";
  private static final int VIEW_EMPTY = 0;
  private static final int VIEW_EDIT_TEXT = 1;
  private static final int VIEW_SIGNATURE = 2;
  private static final int VIEW_UPLOAD = 3;
  private static final int VIEW_DATE_TIME = 4;
  private static final int VIEW_DATE_RANGE = 5;
  private static final int VIEW_DATE = 6;
  private static final int VIEW_DESCRIPTION = 7;
  private static final int VIEW_UNDEFINED = 8;

  ArrayList <FormData> formDataList;
  private Context context;
  private AdapterListener adapterListener;

  public DynamicAdapter(ArrayList <FormData > formDataList) {
   this.formDataList = formDataList;
  }

  public void setAdapterListener(AdapterListener adapterListener) {
   this.adapterListener = adapterListener;
  }
  @NonNull
  @Override
  public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
   this.context = parent.getContext();
   switch (viewType) {
    case VIEW_EDIT_TEXT:
     ItemDynamicFormEdittextBinding edittextBinding = ItemDynamicFormEdittextBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new EditTextViewHolder(edittextBinding);
    case VIEW_NUMBER:
     ItemDynamicFormNumberBinding numberBinding = ItemDynamicFormNumberBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new NumberViewHolder(numberBinding);
    case VIEW_EMAIL:
     ItemDynamicFormEmailBinding emailBinding = ItemDynamicFormEmailBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new EmailViewHolder(emailBinding);
    case VIEW_UPLOAD:
     ItemDynamicFormUploadBinding uploadBinding = ItemDynamicFormUploadBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new UploadViewHolder(uploadBinding);
    case VIEW_DATE_RANGE:
     ItemDynamicFormDateRangeBinding rangeBinding = ItemDynamicFormDateRangeBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new DateRangeViewHolder(rangeBinding);
    case VIEW_DATE_TIME:
     ItemDynamicFormDateTimeBinding dateTimeBinding = ItemDynamicFormDateTimeBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new DateTimeViewHolder(dateTimeBinding);
    case VIEW_DATE:
     ItemDynamicFormDateBinding dateBinding = ItemDynamicFormDateBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new DateViewHolder(dateBinding);
    case VIEW_DESCRIPTION:
     ItemDynamicFormDescriptionBinding descriptionBinding = ItemDynamicFormDescriptionBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new DescriptionViewHolder(descriptionBinding);
    case VIEW_UNDEFINED:
     ItemDynamicFormUnknownBinding unknownBinding = ItemDynamicFormUnknownBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new UnknownViewHolder(unknownBinding);
    default:
     ItemDynamicFormEmptyViewBinding emptyViewBinding = ItemDynamicFormEmptyViewBinding
      .inflate(LayoutInflater.from(parent.getContext()), parent, false);
     return new EmptyViewHolder(emptyViewBinding);
   }
  }
  @Override
  public void onBindViewHolder(@NonNull BaseViewHolder holder, final int position) {
   holder.onBind(position);
  }

  @Override
  public int getItemCount() {
   return formDataList.size();
  }

  @Override
  public int getItemViewType(int position) {
   if (formDataList != null && !formDataList.isEmpty()) {
    if (formDataList.get(position) != null &&
     formDataList.get(position).getType() != null) {
     switch (formDataList.get(position).getType()) {
      case TEXT:
       return VIEW_EDIT_TEXT;
      case CAMERA:
       return VIEW_CAMERA;
      case TOGGLE:
       return VIEW_TOGGLE;
      case NUMBER:
       return VIEW_NUMBER;
      case EMAIL:
       return VIEW_EMAIL;
      case DATE_RANGE:
       return VIEW_DATE_RANGE;
      case DATE_TIME:
       return VIEW_DATE_TIME;
      default:
       return VIEW_UNDEFINED;
     }
    } else {
     return VIEW_UNDEFINED;
    }
   } else {
    return VIEW_EMPTY;
   }
  }

  /**
   * Class used to handle all the text fields for email,text & number.
   */
  private class EditTextViewHolder extends BaseViewHolder {

   ItemDynamicFormEdittextBinding mBinding;

   EditTextViewHolder(ItemDynamicFormEdittextBinding binding) {
    super(binding.getRoot());
    Log.e(TAG, "EditTextViewHolder: ------------>>");
    this.mBinding = binding;
    mBinding.edDynamicFormText.addTextChangedListener(new TextWatcher() {
     @Override
     public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

     }

     @Override
     public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
     }

     @Override
     public void afterTextChanged(Editable editable) {

     }
    });
   }

   @Override
   public void onBind(int position) {
    final FormData formData = formDataList.get(position);
    FormEdittextViewModel emptyItemViewModel = new FormEdittextViewModel(formData);
    mBinding.setViewModel(emptyItemViewModel);

    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }
  }

  /**
   * Class used to handle all the text fields for email, text & number.
   */
  private class NumberViewHolder extends BaseViewHolder {

   ItemDynamicFormNumberBinding mBinding;

   NumberViewHolder(ItemDynamicFormNumberBinding binding) {
    super(binding.getRoot());
    this.mBinding = binding;

    mBinding.edDynamicFormNumber.addTextChangedListener(new TextWatcher() {
     @Override
     public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

     }

     @Override
     public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
     }

     @Override
     public void afterTextChanged(Editable editable) {

     }
    });
   }

   @Override
   public void onBind(int position) {
    final FormData formData = formDataList.get(position);
    mBinding.setViewModel(new FormNumberViewModel(formData));
    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }
  }

  /**
   * Class used to handle all the text fields for email, text & number.
   */
  private class EmailViewHolder extends BaseViewHolder {

   ItemDynamicFormEmailBinding mBinding;

   EmailViewHolder(ItemDynamicFormEmailBinding binding) {
    super(binding.getRoot());
    this.mBinding = binding;
    mBinding.edDynamicFormEmail.addTextChangedListener(new TextWatcher() {
     @Override
     public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

     }

     @Override
     public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
     }

     @Override
     public void afterTextChanged(Editable editable) {

     }
    });
   }

   @Override
   public void onBind(int position) {
    final FormData formData = formDataList.get(position);

    //val radioButton = mBinding.root.findViewById(R.id.radioButtton) as RadioButton
    FormEmailViewModel emptyItemViewModel = new FormEmailViewModel(formData);
    mBinding.setViewModel(emptyItemViewModel);

    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }
  }

  /**
   * Class used to handle all the text fields for description only.
   */
  private class DescriptionViewHolder extends BaseViewHolder {

   ItemDynamicFormDescriptionBinding mBinding;

   DescriptionViewHolder(ItemDynamicFormDescriptionBinding binding) {
    super(binding.getRoot());
    this.mBinding = binding;
    mBinding.edDynamicFormDesc.addTextChangedListener(new TextWatcher() {
     @Override
     public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

     }

     @Override
     public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
      formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
     }

     @Override
     public void afterTextChanged(Editable editable) {

     }
    });
   }

   @Override
   public void onBind(int position) {
    final FormData formData = formDataList.get(position);

    FormDescriptionViewModel emptyItemViewModel = new FormDescriptionViewModel(formData);
    mBinding.setViewModel(emptyItemViewModel);

    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }
  }
  /**
   * Class used to select pic form device and upload to server.
   */
  private class UploadViewHolder extends BaseViewHolder
  implements FormUploadViewModel.UploadListener {
   private ItemDynamicFormUploadBinding mBinding;

   private UploadViewHolder(ItemDynamicFormUploadBinding mBinding) {
    super(mBinding.getRoot());
    this.mBinding = mBinding;
   }

   @Override
   public void onBind(int position) {
    final FormData data = formDataList.get(position);

    FormUploadViewModel uploadViewModel = new FormUploadViewModel(data, this);
    mBinding.setViewModel(uploadViewModel);
    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }

   @Override
   public void onUploadClick(@NonNull FormData formData) {
    adapterListener.onUploadPic(getAdapterPosition(), formData);
   }
  }

  /**
   * Class used to pick date and time.
   */
  private class DateTimeViewHolder extends BaseViewHolder
  implements FormDateTimeViewModel.DateTimeListener {

   ItemDynamicFormDateTimeBinding mBinding;
   int mYear;
   int mMonth;
   int mDay;
   int mHour;
   int mMinute;
   int mSecond;
   FormDateTimeViewModel dateTimeViewModel;
   FormData data;

   DateTimeViewHolder(ItemDynamicFormDateTimeBinding mBinding) {
    super(mBinding.getRoot());
    this.mBinding = mBinding;
   }

   @Override
   public void onBind(int position) {
    data = formDataList.get(position);
    dateTimeViewModel = new FormDateTimeViewModel(data, this);
    mBinding.setViewModel(dateTimeViewModel);
    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }

   @Override
   public void dateClick() {
    Calendar calendar = Calendar.getInstance();
    mHour = calendar.get(Calendar.HOUR_OF_DAY);
    mMinute = calendar.get(Calendar.MINUTE);
    mSecond = 0;
    mYear = calendar.get(Calendar.YEAR);
    mMonth = calendar.get(Calendar.MONTH);
    mDay = calendar.get(Calendar.DAY_OF_MONTH);
    CommonUtils.openDatePicker(context, mYear, mMonth, mDay,
     0, 0, (view, year, monthOfYear, dayOfMonth) -> {
      String selectedDate = dayOfMonth + "-" + (monthOfYear + 1) + "-" + year;
      Calendar cal = Calendar.getInstance();
      cal.set(Calendar.YEAR, year);
      cal.set(Calendar.MONTH, monthOfYear);
      cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
      /*cal.set(Calendar.HOUR_OF_DAY, mHour);
      cal.set(Calendar.MINUTE, mMinute);
      cal.set(Calendar.SECOND, 0);*/

      dateTimeViewModel.getDate().set(DateTimeUtil.getParsedDate(cal.getTimeInMillis()));
      data.setEnteredValue(cal.getTimeInMillis() + "");
     });
   }

   @Override
   public void timeClick() {
    Calendar calendar = Calendar.getInstance();
    mHour = calendar.get(Calendar.HOUR_OF_DAY);
    mMinute = calendar.get(Calendar.MINUTE);

    CommonUtils.openTimePicker(context, mHour, mMinute,
     (view, hourOfDay, minute) -> {

      Calendar cal = Calendar.getInstance();
      cal.set(Calendar.YEAR, mYear);
      cal.set(Calendar.MONTH, mMonth);
      cal.set(Calendar.DAY_OF_MONTH, mDay);
      cal.set(Calendar.HOUR_OF_DAY, hourOfDay);
      cal.set(Calendar.MINUTE, minute);
      cal.set(Calendar.SECOND, 0);
      dateTimeViewModel.getTime().set(DateTimeUtil.getParsedTime(cal.getTimeInMillis()));
      data.setEnteredValue(cal.getTimeInMillis() + "");
     });
   }
  }

  /**
   * Class used to pick date from & to .
   */
  private class DateRangeViewHolder extends BaseViewHolder
  implements FormDateRangeViewModel.DateRangeListener {
   ItemDynamicFormDateRangeBinding mBinding;
   FormDateRangeViewModel uploadViewModel;

   DateRangeViewHolder(ItemDynamicFormDateRangeBinding mBinding) {
    super(mBinding.getRoot());
    this.mBinding = mBinding;
   }

   @Override
   public void onBind(int position) {
    FormData data = formDataList.get(position);

    uploadViewModel = new FormDateRangeViewModel(data, this);
    mBinding.setViewModel(uploadViewModel);
    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }

   @Override
   public void dateViewClick(@NotNull View view) {
    // Get Current Date
    Calendar c = Calendar.getInstance();
    int mYear = c.get(Calendar.YEAR);
    int mMonth = c.get(Calendar.MONTH);
    int mDay = c.get(Calendar.DAY_OF_MONTH);
    //            int mHour = c.get(Calendar.HOUR_OF_DAY);
    //            int mMin = c.get(Calendar.MINUTE);
    CommonUtils.openDatePicker(context, mYear, mMonth, mDay,
     c.getTimeInMillis(), 0, (view1, year, monthOfYear, dayOfMonth) -> {
      //String selectedDate = dayOfMonth + "-" + (monthOfYear + 1) + "-" + year;
      Calendar cal = Calendar.getInstance();
      cal.set(Calendar.YEAR, year);
      cal.set(Calendar.MONTH, monthOfYear);
      cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);

      switch (view.getId()) {
       case R.id.tvDateRange1:
        uploadViewModel.getDate1().set(DateTimeUtil.getParsedDate(cal.getTimeInMillis()));
        formDataList.get(getAdapterPosition()).setMaxRange(cal.getTimeInMillis());
        break;
       case R.id.tvDateRange2:
        uploadViewModel.getDate2().set(DateTimeUtil.getParsedDate(cal.getTimeInMillis()));
        formDataList.get(getAdapterPosition()).setMinRange(cal.getTimeInMillis());
        break;
      }
     });

   }
  }

  /**
   * Class used to pick date
   */
  private class DateViewHolder extends BaseViewHolder
  implements FormDateViewModel.DateListener {
   ItemDynamicFormDateBinding mBinding;
   FormDateViewModel emptyItemViewModel;
   int mYear, mMonth, mDay;

   DateViewHolder(ItemDynamicFormDateBinding mBinding) {
    super(mBinding.getRoot());
    this.mBinding = mBinding;
   }

   @Override
   public void onBind(int position) {
    FormData data = formDataList.get(position);

    emptyItemViewModel = new FormDateViewModel(data, this);
    mBinding.setViewModel(emptyItemViewModel);
    // Immediate Binding
    // When a variable or observable changes, the binding will be scheduled to change before
    // the next frame. There are times, however, when binding must be executed immediately.
    // To force execution, use the executePendingBindings() method.
    mBinding.executePendingBindings();
   }

   @Override
   public void onDateClick() {
    Calendar calendar = Calendar.getInstance();
    mYear = calendar.get(Calendar.YEAR);
    mMonth = calendar.get(Calendar.MONTH);
    mDay = calendar.get(Calendar.DAY_OF_MONTH);
    CommonUtils.openDatePicker(context, mYear, mMonth, mDay,
     0, 0, (view, year, monthOfYear, dayOfMonth) -> {
      //                        String selectedDate = dayOfMonth + "-" + (monthOfYear + 1) + "-" + year;

      Calendar c = Calendar.getInstance();
      c.set(Calendar.YEAR, year);
      c.set(Calendar.MONTH, monthOfYear);
      c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
      formDataList.get(getAdapterPosition()).setMaxRange(c.getTimeInMillis());
      emptyItemViewModel.getDate().set(DateTimeUtil.getParsedDate(c.getTimeInMillis()));

     });
   }
  }

/**
 * If hashMap is empty show empty view
 */
private class EmptyViewHolder extends BaseViewHolder
        implements FormEmptyItemViewModel.ClickListener {

    private ItemDynamicFormEmptyViewBinding mBinding;

    EmptyViewHolder(ItemDynamicFormEmptyViewBinding binding) {
        super(binding.getRoot());
        this.mBinding = binding;
    }

    @Override
    public void onBind(int position) {
        FormEmptyItemViewModel emptyItemViewModel = new FormEmptyItemViewModel(this);
        mBinding.setViewModel(emptyItemViewModel);
    }
}

/**
 * If view type is not handled then show this view
 */
private class UnknownViewHolder extends BaseViewHolder {

    UnknownViewHolder(ItemDynamicFormUnknownBinding unknownBinding) {
        super(unknownBinding.getRoot());
    }

    @Override
    public void onBind(int position) {

    }
}

If you have any doubt let me know, Happy Coding:)

Make the RecyclerView ViewModel non-recyclable. I think the recyclerview is set to refresh by default, therefore, shuffling the items For example

class YourViewModel extends RecyclerView.ViewHolder {

    YourViewModel (@NonNull View view) {
       super(view);

       // Add the line below
       this.setIsRecyclable(false);

     }
}

Also, you could implement a sorting method using Collection.sort(items) before the items are populated, so even when the view gets recycled, the items still remain sorted (eg. by id).

You may try:

@Override
public long getItemId(int position) {
    return position;
}

Also override:

@Override
public int getItemViewType(int position) {
    return position;
}

One way to increase the cache for recyclerview is:

recyclerView.setItemViewCacheSize(20)

You can also get extra space with the help of LinearLayoutManager's method calculateExtraLayoutSpace . I've added links to the docs.

Alternatively you can use:

setIsRecyclable(false);

But it beats the purpose of using RecyclerView.

You should not use hashCode() as an ID as it is not guaranteed to be unique. My guess is that many of your items are returning the same hash. Please try implementing a unique ID for each item and use the it in your getItemId() method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM