BaseRecycler View AdapterHelper 소스 판독 (4) 위에 더 많이 불러오기
위로 당기기 로딩은 미끄럼 이벤트를 감청할 필요가 없고 로딩 레이아웃을 사용자 정의하여 이상 알림을 표시하고 이상 알림을 사용자 정의할 수 있습니다.
이 글은 Base Recycler View Adapter Helper 원본 코드 해석 4편, 원본 라이브러리 주소입니다. 만약에 이전 3편의 글을 보지 못한 학생이 먼저 가보면 대신은 바로 건너갈 수 있습니다.
Base Recycler View Adapter Helper 원본 해독 (1) 간단한 adapter와 만능 Base View Holder 봉인
BaseRecycler ViewAdapterHelper 원본 해독 (2) 헤더와 footer 추가
BaseRecycler ViewAdapterHelper 소스 판독 (3) 애니메이션 추가
오늘BaseRecycler ViewAdapterHelper가 어떻게 애니메이션을 추가하는지 보여 드리겠습니다.본인의 재능과 학문이 아직 얕기 때문에, 만약 잘못된 점이 있으면, 시정을 환영합니다. 감사합니다.
소스 오픈 라이브러리 자동 로드 사용 방법
위로 당겨 싣다
// Item onLoadMoreRequested
setOnLoadMoreListener(RequestLoadMoreListener);
기본 첫 번째 로드는 onLoadMoreRequested () 를 리셋하여 더 많은 메서드를 로드합니다. 필요하지 않으면 다음과 같이 설정할 수 있습니다.
mQuickAdapter.disableLoadMoreIfNotFullPage();
콜백 처리 코드
mQuickAdapter.setOnLoadMoreListener(new BaseQuickAdapter.RequestLoadMoreListener() {
@Override public void onLoadMoreRequested() {
mRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
if (mCurrentCounter >= TOTAL_COUNTER) {
//
mQuickAdapter.loadMoreEnd();
} else {
if (isErr) {
//
mQuickAdapter.addData(DataServer.getSampleData(PAGE_SIZE));
mCurrentCounter = mQuickAdapter.getData().size();
mQuickAdapter.loadMoreComplete();
} else {
//
isErr = true;
Toast.makeText(PullToRefreshUseActivity.this, R.string.network_err, Toast.LENGTH_LONG).show();
mQuickAdapter.loadMoreFail();
}
}
}
}, delayMillis);
}
}, mReyclerView);
, demo,https://github.com/CymChad/BaseRecyclerViewAdapterHelper/blob/d296d1fb4e7a64b9fa8a2601f3f896d3a9518be5/app/src/main/java/com/chad/baserecyclerviewadapterhelper/PullToRefreshUseActivity.java
로드 완료(로드가 끝난 것이 아니라 이번 데이터 로드가 끝났고 다음 페이지의 데이터가 있음을 주의하십시오)
mQuickAdapter.loadMoreComplete();
로드 실패
mQuickAdapter.loadMoreFail();
로드 종료
mQuickAdapter.loadMoreEnd();
감청기 설정, 감청 켜기 로드
/**
* RecyclerView
* @param requestLoadMoreListener
* @param recyclerView
*/
public void setOnLoadMoreListener(RequestLoadMoreListener requestLoadMoreListener,
RecyclerView recyclerView) {
openLoadMore(requestLoadMoreListener);
if (getRecyclerView() == null) {
setRecyclerView(recyclerView);
}
}
/**
*
*
* @param requestLoadMoreListener
*/
private void openLoadMore(RequestLoadMoreListener requestLoadMoreListener) {
this.mRequestLoadMoreListener = requestLoadMoreListener;
mNextLoadEnable = true;
mLoadMoreEnable = true;
mLoading = false;
}
설정은 언제 리셋됩니까?
/**
* N Item ( 1) onLoadMoreRequested()
* @param preLoadNumber
*/
public void setPreLoadNumber(int preLoadNumber) {
if (preLoadNumber > 1) {
mPreLoadNumber = preLoadNumber;
}
}
먼저 간단하게 말하자면 위의 이 방법은 비교적 간단하고 배치형에 속하는 방법이다.목록이 맨 아래 N 번째 Item으로 미끄러질 때 (기본값은 1) 리셋 onLoadMore Requested () 방법을 설정합니다.이따가 아래에 이 매개 변수를 사용할 테니 우선 놓아라.또한 기본값이 있기 때문에 이 방법은 사용할 때 사용할 필요가 없다.
/**
* To bind different types of holder and solve different the bind events
*
* @param holder
* @param position
* @see #getDefItemViewType(int)
*/
@Override
public void onBindViewHolder(K holder, int position) {
//Add up fetch logic, almost like load more, but simpler.
//
autoUpFetch(position);
//Do not move position, need to change before LoadMoreView binding
//
autoLoadMore(position);
int viewType = holder.getItemViewType();
switch (viewType) {
case 0:
convert(holder, getItem(position - getHeaderLayoutCount()));
break;
case LOADING_VIEW:
mLoadMoreView.convert(holder);
break;
case HEADER_VIEW:
break;
case EMPTY_VIEW:
break;
case FOOTER_VIEW:
break;
default:
convert(holder, getItem(position - getHeaderLayoutCount()));
break;
}
}
/**
* position
*
* @param position onBindViewHolder() Position
*/
private void autoLoadMore(int position) {
//
if (getLoadMoreViewCount() == 0) {
return;
}
// mPreLoadNumber item
if (position < getItemCount() - mPreLoadNumber) {
return;
}
// , ( ),
if (mLoadMoreView.getLoadMoreStatus() != LoadMoreView.STATUS_DEFAULT) {
return;
}
// :
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_LOADING);
if (!mLoading) {
mLoading = true;
if (getRecyclerView() != null) {
getRecyclerView().post(new Runnable() {
@Override
public void run() {
//
mRequestLoadMoreListener.onLoadMoreRequested();
}
});
} else {
mRequestLoadMoreListener.onLoadMoreRequested();
}
}
}
/**
* Load more view count
*
* @return 0 or 1
*/
public int getLoadMoreViewCount() {
//
if (mRequestLoadMoreListener == null || !mLoadMoreEnable) {
return 0;
}
//
if (!mNextLoadEnable && mLoadMoreView.isLoadEndMoreGone()) {
return 0;
}
//
if (mData.size() == 0) {
return 0;
}
return 1;
}
중점은 더 많은 주요 논리를 불러오는 것이다. 온빈드 뷰 Holder () 에서 현재 item의position 위치에 따라 더 많은 불러오는 것을 실행해야 하는지 판단하는 것이다.구체적인 판단 논리: item이 처음 window 인터페이스에 들어갈 때 onBindViewHolder () 를 호출하여 데이터를 연결합니다. 이 때 이position의 위치를 알고 있습니다. 그래서 우리는 이렇게 할 수 있습니다.onBindViewHolder () 가 데이터를 연결할 때position이 마지막 mPreLoadNumber 개일 때, 우리는 더 많은 리셋을 불러오고 호출자가 처리하도록 합니다.
물론, 리셋하기 전에 우리는 현재 더 많은 로드를 할 수 있는지를 확인하는 판단을 해야 한다.-mRequestLoadMoreListener 감청기가null인지, 현재 더 많은 데이터를 불러올 수 있는 상태인지 (mLoadMoreEnable 로고 비트 제어) - 현재 더 많은 데이터가 있는지 (외부 호출자에 의해 결정됨) - 현재 데이터 항목의 개수가 0인지, 데이터 항목이 없으면그러면 더 많이 불러올 필요가 없습니다. - 꼴찌에 들어간 mPreLoadNumber 영역에 들어갈지 여부 - 현재 (mLoadMoreView 이것은 더 많은 View를 불러오는 것) 불러오는 상태를 판단합니다. 기본 상태가 아니면 불러오는 중일 수도 있습니다.
알겠습니다. 세심한 관중들은 위의 이런 방식은 사실 단점이 하나 있습니다. 데이터 항목의 개수가 1스크린보다 적으면 마지막 밑에 있는 mPreLoadNumber개는 분명히 볼 수 있습니다. 이왕 볼 수 있는 이상 이item의 onBindViewHolder()를 실행할 것입니다. 이 방법을 실행하면 더 많은 로드가 필요한지 판단할 수 있습니다. 분명히 이때는 조건에 부합됩니다.따라서 데이터가 화면을 가득 채우지 않으면 자동으로 onLoadMore Requested () 로 리셋되고 불러오는 중이라고 표시됩니다.
분명히 이때는 우리의 요구에 부합되지 않는다.그래서 정부측에 해결 방안이 하나 있다.아래를 내려다보다.
/**
* bind recyclerView {@link #bindToRecyclerView(RecyclerView)} before use!
*
* @see #disableLoadMoreIfNotFullPage(RecyclerView)
*/
public void disableLoadMoreIfNotFullPage() {
// RecyclerView null
checkNotNull();
disableLoadMoreIfNotFullPage(getRecyclerView());
}
/**
* check if full page after {@link #setNewData(List)}, if full, it will enable load more again.
*
* !!
*
* , {@link #setNewData(List)}
* : load more,
* , load more
* > , load more
*
* !!
*
* @param recyclerView your recyclerView
* @see #setNewData(List)
*/
public void disableLoadMoreIfNotFullPage(RecyclerView recyclerView) {
// false
setEnableLoadMore(false);
if (recyclerView == null) return;
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager == null) return;
if (manager instanceof LinearLayoutManager) {
final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) manager;
recyclerView.postDelayed(new Runnable() {
@Override
public void run() {
// > , load more
if ((linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1) !=
getItemCount()) {
setEnableLoadMore(true);
}
}
}, 50);
} else if (manager instanceof StaggeredGridLayoutManager) {
final StaggeredGridLayoutManager staggeredGridLayoutManager =
(StaggeredGridLayoutManager) manager;
recyclerView.postDelayed(new Runnable() {
@Override
public void run() {
// StaggeredGridLayoutManager
final int[] positions = new int[staggeredGridLayoutManager.getSpanCount()];
// ( ) item
staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(positions);
// ( StaggeredGridLayoutManager item)
int pos = getTheBiggestNumber(positions) + 1;
// > , load more
if (pos != getItemCount()) {
setEnableLoadMore(true);
}
}
}, 50);
}
}
/**
*
* @param numbers
* @return
*/
private int getTheBiggestNumber(int[] numbers) {
int tmp = -1;
if (numbers == null || numbers.length == 0) {
return tmp;
}
for (int num : numbers) {
if (num > tmp) {
tmp = num;
}
}
return tmp;
}
/**
* Set the enabled state of load more.
*
*
* @param enable True if load more is enabled, false otherwise.
*/
public void setEnableLoadMore(boolean enable) {
//
int oldLoadMoreCount = getLoadMoreViewCount();
mLoadMoreEnable = enable;
int newLoadMoreCount = getLoadMoreViewCount();
if (oldLoadMoreCount == 1) {
if (newLoadMoreCount == 0) {
//
notifyItemRemoved(getLoadMoreViewPosition());
}
} else {
if (newLoadMoreCount == 1) {
//
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
notifyItemInserted(getLoadMoreViewPosition());
}
}
}
이 코드는 소스 오픈 프로젝트의 토론 구역에서 매우 핫한 것을 보았는데, 마치 많은 사람들이 disable Load More If Not FullPage () 를 사용하여 무효한 사건을 만난 것 같다.그들이 잘못 썼나 봐, 아마.disable LoadMoreIfNotFullPage () 는 setNew Data () 이후에 호출해야 유효합니다.disable Load More If Not Full Page () 에서 하고 싶은 일은 load more가 필요한지 판단하는 것입니다. 그의 판단 근거는 현재 화면 안의 맨 밑에 있는 item의 인덱스가 전체 데이터 항목의 개수와 같은지 확인하는 것입니다. -만약 같다면 한 화면이 가득 차지 않았음을 의미하며,load more를 켤 필요가 없습니다. - 같지 않다면 한 화면이 가득 찼음을 의미하며,laod more를 켜야 합니다.
마운트 레이아웃 item을 만들고 마운트 레이아웃의 클릭 이벤트를 설정합니다
@Override
public K onCreateViewHolder(ViewGroup parent, int viewType) {
K baseViewHolder = null;
this.mContext = parent.getContext();
this.mLayoutInflater = LayoutInflater.from(mContext);
switch (viewType) {
case LOADING_VIEW:
baseViewHolder = getLoadingView(parent);
break;
case HEADER_VIEW:
baseViewHolder = createBaseViewHolder(mHeaderLayout);
break;
case EMPTY_VIEW:
baseViewHolder = createBaseViewHolder(mEmptyLayout);
break;
case FOOTER_VIEW:
baseViewHolder = createBaseViewHolder(mFooterLayout);
break;
default:
baseViewHolder = onCreateDefViewHolder(parent, viewType);
bindViewClickListener(baseViewHolder);
}
baseViewHolder.setAdapter(this);
return baseViewHolder;
}
private K getLoadingView(ViewGroup parent) {
//
View view = getItemView(mLoadMoreView.getLayoutId(), parent);
// baseviewholder
K holder = createBaseViewHolder(view);
//
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_FAIL) {
//
notifyLoadMoreToLoading();
}
if (mEnableLoadMoreEndClick && mLoadMoreView.getLoadMoreStatus() == LoadMoreView
.STATUS_END) {
//
notifyLoadMoreToLoading();
}
}
});
return holder;
}
/**
* The notification starts the callback and loads more
*
*/
public void notifyLoadMoreToLoading() {
// ,
if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_LOADING) {
return;
}
// adapter onBindViewHolder()
//autoLoadMore() , , onLoadMoreRequested()
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
notifyItemChanged(getLoadMoreViewPosition());
}
이곳의 목표는 더 많은 레이아웃을 불러오는 것이다. 클릭 이벤트를 설정하는 것이다. 코드에서 더 많은 레이아웃을 불러오는 것은 클릭 이벤트를 직접 설정하는 것이다. 단지 서로 다른 상태에 따라 더 많은 논리를 실행해야 하는지를 결정할 뿐이다.다음 두 가지 상황만 더 많이 불러와야 합니다. -이전에 불러오는 데 실패한 상태였을 때 불러오는 레이아웃이 눌렸습니다. - 이전에는 끝난 상태였고 더 많은 레이아웃을 불러오면 눌릴 수 있습니다.
이 두 가지 상황을 만족시킬 때, 레이아웃view를 불러오는 상태를 기본 상태로 설정하고, adapter의 마지막 항목 (즉 더 많은 레이아웃을 불러오는 것) 을 리셋합니다. 그러면 adapter는 onBindViewHolder () 를 리셋하고, onBindViewHolder () 는 autoLoadMore () 방법을 사용해서 더 많이 불러올 필요가 있는지 판단합니다. 이것은 조건에 부합되는 것이기 때문에 리셋이 필요합니다.따라서 onLoadMoreRequested () 를 리셋하고 마운트 레이아웃의 상태를 STATUSLOADING가 불러오는 상태입니다. 이렇게 하면 불러오는 레이아웃의 스타일도 바뀝니다.
로드 완료
마운트가 끝난 것이 아니라 이번 데이터 마운트가 끝났고 다음 페이지의 데이터가 있음을 주의하십시오
/**
* Refresh complete
*
*
*/
public void loadMoreComplete() {
if (getLoadMoreViewCount() == 0) {
return;
}
// false
mLoading = false;
//
mNextLoadEnable = true;
//
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
// ,
notifyItemChanged(getLoadMoreViewPosition());
}
/**
* Gets to load more locations
*
* @return
*/
public int getLoadMoreViewPosition() {
return getHeaderLayoutCount() + mData.size() + getFooterLayoutCount();
}
리셋이 끝난 후에 뒷수습을 해야 한다. 위에서 보듯이 코드 주석은 이미 매우 명확하다.
로드 실패
/**
* Refresh failed
*
*/
public void loadMoreFail() {
if (getLoadMoreViewCount() == 0) {
return;
}
//
mLoading = false;
//
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_FAIL);
// ,
notifyItemChanged(getLoadMoreViewPosition());
}
계속해서 불러올 수 있는지 간단하게 판단하고 레이아웃을 업데이트하는 것입니다.
로드 종료
/**
* Refresh end, no more data
* ,
*
*/
public void loadMoreEnd() {
loadMoreEnd(false);
}
/**
* Refresh end, no more data
* ,
* gone: true: false:
* @param gone if true gone the load more view
*/
public void loadMoreEnd(boolean gone) {
if (getLoadMoreViewCount() == 0) {
return;
}
mLoading = false;
//
mNextLoadEnable = false;
//
mLoadMoreView.setLoadMoreEndGone(gone);
if (gone) {
// ,
notifyItemRemoved(getLoadMoreViewPosition());
} else {
// , ( STATUS_END )
mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_END);
// adapter
notifyItemChanged(getLoadMoreViewPosition());
}
}
설정 불러오기가 끝났습니다. 더 이상 불러올 데이터가 없다는 뜻입니다. 따라서 mNextLoadEnable 로고 위치를false로 설정하면 다음 페이지를 불러올 수 없습니다.그리고 로드 레이아웃을 표시할지 여부에 따라 adapter를 새로 고칩니다.
위로 당기기 로드 레이아웃
원본 코드 안에 추상적인 종류인 LoadMoreView가 있습니다.
public abstract class LoadMoreView {
public static final int STATUS_DEFAULT = 1;
/**
*
*/
public static final int STATUS_LOADING = 2;
/**
*
*/
public static final int STATUS_FAIL = 3;
/**
*
*/
public static final int STATUS_END = 4;
/**
*
*/
private int mLoadMoreStatus = STATUS_DEFAULT;
private boolean mLoadMoreEndGone = false;
public void setLoadMoreStatus(int loadMoreStatus) {
this.mLoadMoreStatus = loadMoreStatus;
}
public int getLoadMoreStatus() {
return mLoadMoreStatus;
}
public void convert(BaseViewHolder holder) {
//
switch (mLoadMoreStatus) {
case STATUS_LOADING:
visibleLoading(holder, true);
visibleLoadFail(holder, false);
visibleLoadEnd(holder, false);
break;
case STATUS_FAIL:
visibleLoading(holder, false);
visibleLoadFail(holder, true);
visibleLoadEnd(holder, false);
break;
case STATUS_END:
visibleLoading(holder, false);
visibleLoadFail(holder, false);
visibleLoadEnd(holder, true);
break;
case STATUS_DEFAULT:
visibleLoading(holder, false);
visibleLoadFail(holder, false);
visibleLoadEnd(holder, false);
break;
}
}
private void visibleLoading(BaseViewHolder holder, boolean visible) {
holder.setVisible(getLoadingViewId(), visible);
}
private void visibleLoadFail(BaseViewHolder holder, boolean visible) {
holder.setVisible(getLoadFailViewId(), visible);
}
private void visibleLoadEnd(BaseViewHolder holder, boolean visible) {
final int loadEndViewId = getLoadEndViewId();
if (loadEndViewId != 0) {
holder.setVisible(loadEndViewId, visible);
}
}
/**
*
* @param loadMoreEndGone true:
*/
public final void setLoadMoreEndGone(boolean loadMoreEndGone) {
this.mLoadMoreEndGone = loadMoreEndGone;
}
public final boolean isLoadEndMoreGone() {
if (getLoadEndViewId() == 0) {
return true;
}
return mLoadMoreEndGone;
}
/**
* No more data is hidden
*
* @return true for no more data hidden load more
* @deprecated Use {@link BaseQuickAdapter#loadMoreEnd(boolean)} instead.
*/
@Deprecated
public boolean isLoadEndGone() {
return mLoadMoreEndGone;
}
/**
* load more layout
*
* @return
*/
public abstract
@LayoutRes
int getLayoutId();
/**
* loading view
*
* @return
*/
protected abstract
@IdRes
int getLoadingViewId();
/**
* load fail view
*
* @return
*/
protected abstract
@IdRes
int getLoadFailViewId();
/**
* load end view, you can return 0
*
* @return
*/
protected abstract
@IdRes
int getLoadEndViewId();
}
이 종류는 불러오는 레이아웃을 관리하는 데 사용되며, 서로 다른 상태에서 서로 다른 레이아웃을 표시합니다.원본 코드에 기본 마운트 레이아웃이 있습니다. 직접 사용할 수 있습니다. 물론 사용자 정의를 지원합니다. Load MoreView만 계승하면 됩니다.
기본 로드 레이아웃은 다음과 같습니다.
public final class SimpleLoadMoreView extends LoadMoreView {
@Override
public int getLayoutId() {
return R.layout.quick_view_load_more;
}
@Override
protected int getLoadingViewId() {
return R.id.load_more_loading_view;
}
@Override
protected int getLoadFailViewId() {
return R.id.load_more_load_fail_view;
}
@Override
protected int getLoadEndViewId() {
return R.id.load_more_load_end_view;
}
}
다음은 xml
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_40">
<LinearLayout
android:id="@+id/load_more_loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/loading_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleSmall"
android:layout_marginRight="@dimen/dp_4"/>
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_4"
android:text="@string/loading"
android:textColor="@android:color/black"
android:textSize="@dimen/sp_14"/>
LinearLayout>
<FrameLayout
android:id="@+id/load_more_load_fail_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<TextView
android:id="@+id/tv_prompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/load_failed"/>
FrameLayout>
<FrameLayout
android:id="@+id/load_more_load_end_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/load_end"
android:textColor="@android:color/darker_gray"/>
FrameLayout>
FrameLayout>
사실은 하나의 레이아웃으로 상태에 따라 어떤 용기를 동적으로 표시하고 숨기면 된다.
총결산
이 부분은 좀 복잡한 것 같아서 이틀의 자질구레한 시간을 들여서야 다 보았는데 아마도 비교적 반찬이기 때문일 것이다.대체적으로 사고방식을 실현하는 것은RecyclerView의 마지막 역수 N항에 미끄러진 것을 검출할 때 리셋을 시작하고 로드 레이아웃과 리셋 인터페이스를 표시하여 외부로 리셋을 실현하도록 하는 것이다.이치는 간단하지만 실현되면 많은 세부 사항이 안에 있고 많은 구덩이가 있습니다. 다시 한 번 개원고의 저자들에게 감사드립니다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Bitrise에서 배포 어플리케이션 설정 테스트하기이 글은 Bitrise 광고 달력의 23일째 글입니다. 자체 또는 당사 등에서 Bitrise 구축 서비스를 사용합니다. 그나저나 며칠 전 Bitrise User Group Meetup #3에서 아래 슬라이드를 발표했...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.