From e3ce57f6a2f58733e05c37407d95050e32ff01df Mon Sep 17 00:00:00 2001 From: FongMi Date: Fri, 21 Oct 2022 18:30:19 +0800 Subject: [PATCH] Use leanback for live --- .../android/tv/ui/activity/LiveActivity.java | 56 +++++++-- .../android/tv/ui/adapter/ChannelAdapter.java | 110 ------------------ .../android/tv/ui/adapter/GroupAdapter.java | 90 -------------- .../tv/ui/adapter/holder/ChannelHolder.java | 43 ------- .../tv/ui/adapter/holder/GroupHolder.java | 35 ------ .../tv/ui/presenter/ChannelPresenter.java | 53 +++++++++ .../tv/ui/presenter/GroupPresenter.java | 52 +++++++++ .../res/drawable/selector_channel.xml | 2 +- .../leanback/res/drawable/selector_group.xml | 2 +- app/src/leanback/res/layout/activity_live.xml | 11 +- .../leanback/res/layout/adapter_channel.xml | 1 + app/src/leanback/res/layout/adapter_group.xml | 3 +- .../com/fongmi/android/tv/bean/Channel.java | 10 +- .../com/fongmi/android/tv/bean/Group.java | 11 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 15 files changed, 170 insertions(+), 311 deletions(-) delete mode 100644 app/src/leanback/java/com/fongmi/android/tv/ui/adapter/ChannelAdapter.java delete mode 100644 app/src/leanback/java/com/fongmi/android/tv/ui/adapter/GroupAdapter.java delete mode 100644 app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/ChannelHolder.java delete mode 100644 app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/GroupHolder.java create mode 100644 app/src/leanback/java/com/fongmi/android/tv/ui/presenter/ChannelPresenter.java create mode 100644 app/src/leanback/java/com/fongmi/android/tv/ui/presenter/GroupPresenter.java diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/LiveActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/LiveActivity.java index 99431310c..028e75931 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/LiveActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/LiveActivity.java @@ -6,6 +6,12 @@ import android.os.Handler; import android.os.Looper; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.leanback.widget.ArrayObjectAdapter; +import androidx.leanback.widget.ItemBridgeAdapter; +import androidx.leanback.widget.OnChildViewHolderSelectedListener; +import androidx.recyclerview.widget.RecyclerView; import androidx.viewbinding.ViewBinding; import com.fongmi.android.tv.api.LiveConfig; @@ -14,8 +20,8 @@ import com.fongmi.android.tv.bean.Group; import com.fongmi.android.tv.databinding.ActivityLiveBinding; import com.fongmi.android.tv.event.PlayerEvent; import com.fongmi.android.tv.player.Players; -import com.fongmi.android.tv.ui.adapter.ChannelAdapter; -import com.fongmi.android.tv.ui.adapter.GroupAdapter; +import com.fongmi.android.tv.ui.presenter.ChannelPresenter; +import com.fongmi.android.tv.ui.presenter.GroupPresenter; import com.fongmi.android.tv.utils.Prefers; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; @@ -25,11 +31,11 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; -public class LiveActivity extends BaseActivity implements GroupAdapter.OnItemClickListener, ChannelAdapter.OnItemClickListener { +public class LiveActivity extends BaseActivity implements GroupPresenter.OnClickListener, ChannelPresenter.OnClickListener { private ActivityLiveBinding mBinding; - private ChannelAdapter mChannelAdapter; - private GroupAdapter mGroupAdapter; + private ArrayObjectAdapter mChannelAdapter; + private ArrayObjectAdapter mGroupAdapter; private Handler mHandler; private Players mPlayers; @@ -41,6 +47,14 @@ public class LiveActivity extends BaseActivity implements GroupAdapter.OnItemCli return Prefers.getRender() == 0 ? mBinding.surface : mBinding.texture; } + private Group getGroup() { + return (Group) mGroupAdapter.get(mBinding.group.getSelectedPosition()); + } + + private Channel getChannel() { + return (Channel) mChannelAdapter.get(mBinding.channel.getSelectedPosition()); + } + @Override protected ViewBinding getBinding() { return mBinding = ActivityLiveBinding.inflate(getLayoutInflater()); @@ -52,17 +66,33 @@ public class LiveActivity extends BaseActivity implements GroupAdapter.OnItemCli mPlayers = new Players().init(); setRecyclerView(); setVideoView(); + getLive(); } @Override protected void initEvent() { EventBus.getDefault().register(this); + mBinding.group.addOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() { + @Override + public void onChildViewHolderSelected(@NonNull RecyclerView parent, @Nullable RecyclerView.ViewHolder child, int position, int subposition) { + onItemClick((Group) mGroupAdapter.get(position)); + } + }); + mBinding.channel.addOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() { + @Override + public void onChildViewHolderSelected(@NonNull RecyclerView parent, @Nullable RecyclerView.ViewHolder child, int position, int subposition) { + onItemClick((Channel) mChannelAdapter.get(position)); + } + }); } private void setRecyclerView() { - mBinding.group.setAdapter(mGroupAdapter = new GroupAdapter(this)); - mBinding.channel.setAdapter(mChannelAdapter = new ChannelAdapter(this)); - mGroupAdapter.addAll(LiveConfig.get().getLives().get(0).getGroups()); + mBinding.group.setAdapter(new ItemBridgeAdapter(mGroupAdapter = new ArrayObjectAdapter(new GroupPresenter(this)))); + mBinding.channel.setAdapter(new ItemBridgeAdapter(mChannelAdapter = new ArrayObjectAdapter(new ChannelPresenter(this)))); + } + + private void getLive() { + mGroupAdapter.setItems(LiveConfig.get().getLives().get(0).getGroups(), null); } private void setVideoView() { @@ -73,11 +103,13 @@ public class LiveActivity extends BaseActivity implements GroupAdapter.OnItemCli @Override public void onItemClick(Group item) { - mChannelAdapter.addAll(item); + mChannelAdapter.setItems(item.getChannel(), null); + mBinding.channel.setSelectedPosition(item.getPosition()); } @Override public void onItemClick(Channel item) { + getGroup().setPosition(mBinding.channel.getSelectedPosition()); mPlayers.start(item); } @@ -99,13 +131,13 @@ public class LiveActivity extends BaseActivity implements GroupAdapter.OnItemCli break; default: if (!event.isRetry() || mPlayers.addRetry() > 1) onError(); - else mPlayers.start(mChannelAdapter.getCurrent()); + //else mPlayers.start(mChannelAdapter.getCurrent()); break; } } private void onError() { - int index = mChannelAdapter.getCurrent().getIndex() + 1; + /*int index = mChannelAdapter.getCurrent().getIndex() + 1; int size = mChannelAdapter.getCurrent().getUrls().size(); mPlayers.setRetry(0); if (index == size) { @@ -114,7 +146,7 @@ public class LiveActivity extends BaseActivity implements GroupAdapter.OnItemCli } else { mChannelAdapter.getCurrent().setIndex(index); mPlayers.start(mChannelAdapter.getCurrent()); - } + }*/ } @Override diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/ChannelAdapter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/ChannelAdapter.java deleted file mode 100644 index 483db5730..000000000 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/ChannelAdapter.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.fongmi.android.tv.ui.adapter; - -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.fongmi.android.tv.bean.Channel; -import com.fongmi.android.tv.bean.Group; -import com.fongmi.android.tv.databinding.AdapterChannelBinding; -import com.fongmi.android.tv.ui.adapter.holder.ChannelHolder; - -import java.util.ArrayList; -import java.util.List; - -public class ChannelAdapter extends RecyclerView.Adapter { - - private OnItemClickListener mListener; - private final List mItems; - private Channel current; - private boolean focus; - private int position; - private Group group; - - public ChannelAdapter(OnItemClickListener listener) { - this.mItems = new ArrayList<>(); - this.mListener = listener; - } - - public interface OnItemClickListener { - - void onItemClick(Channel item); - } - - private Channel getItem() { - return mItems.get(position); - } - - public Channel getCurrent() { - return current; - } - - public void setCurrent(Channel current) { - this.current = current; - } - - public boolean isFocus() { - return focus; - } - - public void setFocus(boolean focus) { - this.focus = focus; - } - - public int getPosition() { - return position; - } - - public void setPosition(int position) { - this.position = position; - } - - public Group getGroup() { - return group; - } - - public void setGroup(Group group) { - this.group = group; - } - - public void addAll(Group group) { - setGroup(group); - mItems.clear(); - mItems.addAll(group.getChannel()); - notifyDataSetChanged(); - } - - public void setSelected() { - for (int i = 0; i < mItems.size(); i++) mItems.get(i).setSelect(i == position); - notifyDataSetChanged(); - setFocus(true); - } - - public void setChannel() { - if (position < 0 || position > mItems.size() - 1) return; - //if (!getGroup().isHidden()) getItem().putKeep(); - mListener.onItemClick(getItem()); - getGroup().setPosition(position); - getItem().setGroup(getGroup()); - setCurrent(getItem()); - setSelected(); - } - - @Override - public int getItemCount() { - return mItems.size(); - } - - @NonNull - @Override - public ChannelHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ChannelHolder(this, AdapterChannelBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull ChannelHolder holder, int position) { - holder.setView(mItems.get(position)); - } -} \ No newline at end of file diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/GroupAdapter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/GroupAdapter.java deleted file mode 100644 index f8e8182c8..000000000 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/GroupAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.fongmi.android.tv.ui.adapter; - -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.fongmi.android.tv.bean.Group; -import com.fongmi.android.tv.databinding.AdapterGroupBinding; -import com.fongmi.android.tv.ui.adapter.holder.GroupHolder; - -import java.util.ArrayList; -import java.util.List; - -public class GroupAdapter extends RecyclerView.Adapter { - - private OnItemClickListener mListener; - private final List mItems; - private final List mHides; - private boolean focus; - private int position; - - public GroupAdapter(OnItemClickListener listener) { - this.mItems = new ArrayList<>(); - this.mHides = new ArrayList<>(); - this.mListener = listener; - } - - public interface OnItemClickListener { - - void onItemClick(Group item); - } - - private Group getItem() { - return mItems.get(position); - } - - public boolean isFocus() { - return focus; - } - - public void setFocus(boolean focus) { - this.focus = focus; - } - - public int getPosition() { - return position; - } - - public void setPosition(int position) { - this.position = position; - } - - public void addAll(List items) { - mItems.clear(); - addGroup(items); - notifyDataSetChanged(); - } - - private void addGroup(List items) { - for (Group item : items) if (item.isHidden()) mHides.add(item); else mItems.add(item); - } - - public void setSelected() { - for (int i = 0; i < mItems.size(); i++) mItems.get(i).setSelect(i == position); - notifyDataSetChanged(); - setFocus(true); - } - - public void setType() { - mListener.onItemClick(getItem()); - } - - @Override - public int getItemCount() { - return mItems.size(); - } - - @NonNull - @Override - public GroupHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new GroupHolder(this, AdapterGroupBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull GroupHolder holder, int position) { - holder.setView(mItems.get(position)); - } -} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/ChannelHolder.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/ChannelHolder.java deleted file mode 100644 index a4067316f..000000000 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/ChannelHolder.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.fongmi.android.tv.ui.adapter.holder; - -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.fongmi.android.tv.bean.Channel; -import com.fongmi.android.tv.databinding.AdapterChannelBinding; -import com.fongmi.android.tv.ui.adapter.ChannelAdapter; - -public class ChannelHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { - - private final AdapterChannelBinding binding; - private final ChannelAdapter adapter; - - public ChannelHolder(ChannelAdapter adapter, @NonNull AdapterChannelBinding binding) { - super(binding.getRoot()); - this.binding = binding; - this.adapter = adapter; - itemView.setOnClickListener(this); - itemView.setOnLongClickListener(this); - } - - @Override - public void onClick(View view) { - adapter.setPosition(getLayoutPosition()); - adapter.setChannel(); - } - - @Override - public boolean onLongClick(View view) { - adapter.setPosition(getLayoutPosition()); - return false; - } - - public void setView(Channel item) { - itemView.setSelected(item.isSelect()); - binding.name.setText(item.getName()); - binding.number.setText(item.getNumber()); - binding.icon.setVisibility(item.getVisible()); - } -} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/GroupHolder.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/GroupHolder.java deleted file mode 100644 index db5230331..000000000 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/holder/GroupHolder.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.fongmi.android.tv.ui.adapter.holder; - -import android.view.View; - -import androidx.recyclerview.widget.RecyclerView; - -import com.fongmi.android.tv.bean.Group; -import com.fongmi.android.tv.databinding.AdapterGroupBinding; -import com.fongmi.android.tv.ui.adapter.GroupAdapter; - -public class GroupHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - - private final AdapterGroupBinding binding; - private final GroupAdapter adapter; - - public GroupHolder(GroupAdapter adapter, AdapterGroupBinding binding) { - super(binding.getRoot()); - this.binding = binding; - this.adapter = adapter; - itemView.setOnClickListener(this); - } - - @Override - public void onClick(View view) { - adapter.setPosition(getLayoutPosition()); - adapter.setSelected(); - adapter.setType(); - } - - public void setView(Group item) { - itemView.setSelected(item.isSelect()); - binding.name.setText(item.getName()); - binding.icon.setVisibility(item.getVisible()); - } -} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/presenter/ChannelPresenter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/presenter/ChannelPresenter.java new file mode 100644 index 000000000..9c2c603d5 --- /dev/null +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/presenter/ChannelPresenter.java @@ -0,0 +1,53 @@ +package com.fongmi.android.tv.ui.presenter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.leanback.widget.Presenter; + +import com.fongmi.android.tv.bean.Channel; +import com.fongmi.android.tv.databinding.AdapterChannelBinding; + +public class ChannelPresenter extends Presenter { + + private final OnClickListener mListener; + + public ChannelPresenter(OnClickListener listener) { + this.mListener = listener; + } + + public interface OnClickListener { + void onItemClick(Channel item); + } + + @Override + public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new ViewHolder(AdapterChannelBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object object) { + Channel item = (Channel) object; + ViewHolder holder = (ViewHolder) viewHolder; + holder.binding.name.setText(item.getName()); + holder.binding.number.setText(item.getNumber()); + holder.binding.icon.setVisibility(item.getVisible()); + holder.binding.getRoot().setActivated(item.isActivated()); + setOnClickListener(holder, view -> mListener.onItemClick(item)); + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { + } + + public static class ViewHolder extends Presenter.ViewHolder { + + private final AdapterChannelBinding binding; + + public ViewHolder(@NonNull AdapterChannelBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} \ No newline at end of file diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/presenter/GroupPresenter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/presenter/GroupPresenter.java new file mode 100644 index 000000000..d422d62c7 --- /dev/null +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/presenter/GroupPresenter.java @@ -0,0 +1,52 @@ +package com.fongmi.android.tv.ui.presenter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.leanback.widget.Presenter; + +import com.fongmi.android.tv.bean.Group; +import com.fongmi.android.tv.databinding.AdapterGroupBinding; + +public class GroupPresenter extends Presenter { + + private final OnClickListener mListener; + + public GroupPresenter(OnClickListener listener) { + this.mListener = listener; + } + + public interface OnClickListener { + void onItemClick(Group item); + } + + @Override + public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new ViewHolder(AdapterGroupBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object object) { + Group item = (Group) object; + ViewHolder holder = (ViewHolder) viewHolder; + holder.binding.name.setText(item.getName()); + holder.binding.icon.setVisibility(item.getVisible()); + holder.binding.getRoot().setActivated(item.isActivated()); + setOnClickListener(holder, view -> mListener.onItemClick(item)); + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { + } + + public static class ViewHolder extends Presenter.ViewHolder { + + private final AdapterGroupBinding binding; + + public ViewHolder(@NonNull AdapterGroupBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} \ No newline at end of file diff --git a/app/src/leanback/res/drawable/selector_channel.xml b/app/src/leanback/res/drawable/selector_channel.xml index d19398eac..ed81966ae 100644 --- a/app/src/leanback/res/drawable/selector_channel.xml +++ b/app/src/leanback/res/drawable/selector_channel.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/leanback/res/drawable/selector_group.xml b/app/src/leanback/res/drawable/selector_group.xml index 1be414c2c..c00fd0f17 100644 --- a/app/src/leanback/res/drawable/selector_group.xml +++ b/app/src/leanback/res/drawable/selector_group.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/leanback/res/layout/activity_live.xml b/app/src/leanback/res/layout/activity_live.xml index 402afadc1..ec603f81b 100644 --- a/app/src/leanback/res/layout/activity_live.xml +++ b/app/src/leanback/res/layout/activity_live.xml @@ -33,14 +33,12 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:background="@drawable/shape_live" - android:minWidth="400dp" android:orientation="horizontal"> - @@ -49,11 +47,10 @@ android:layout_height="match_parent" android:background="@color/grey_700" /> - diff --git a/app/src/leanback/res/layout/adapter_channel.xml b/app/src/leanback/res/layout/adapter_channel.xml index 8fc9c8d08..18eb55f7b 100644 --- a/app/src/leanback/res/layout/adapter_channel.xml +++ b/app/src/leanback/res/layout/adapter_channel.xml @@ -5,6 +5,7 @@ android:layout_height="wrap_content" android:background="@drawable/selector_channel" android:focusable="true" + android:focusableInTouchMode="true" android:gravity="center" android:orientation="horizontal" android:paddingStart="20dp" diff --git a/app/src/leanback/res/layout/adapter_group.xml b/app/src/leanback/res/layout/adapter_group.xml index c0afd253c..5de250fad 100644 --- a/app/src/leanback/res/layout/adapter_group.xml +++ b/app/src/leanback/res/layout/adapter_group.xml @@ -5,7 +5,8 @@ android:layout_height="wrap_content" android:background="@drawable/selector_group" android:focusable="true" - android:gravity="center_vertical" + android:focusableInTouchMode="true" + android:gravity="center" android:orientation="horizontal" android:paddingStart="20dp" android:paddingTop="12dp" diff --git a/app/src/main/java/com/fongmi/android/tv/bean/Channel.java b/app/src/main/java/com/fongmi/android/tv/bean/Channel.java index a34177e44..1d53152c5 100644 --- a/app/src/main/java/com/fongmi/android/tv/bean/Channel.java +++ b/app/src/main/java/com/fongmi/android/tv/bean/Channel.java @@ -31,8 +31,8 @@ public class Channel { @SerializedName("ua") private String ua; + private boolean activated; private int index; - private boolean select; public static Channel objectFrom(JsonElement element) { return new Gson().fromJson(element, Channel.class); @@ -112,12 +112,12 @@ public class Channel { this.index = index; } - public boolean isSelect() { - return select; + public boolean isActivated() { + return activated; } - public void setSelect(boolean select) { - this.select = select; + public void setActivated(boolean activated) { + this.activated = activated; } public int getVisible() { diff --git a/app/src/main/java/com/fongmi/android/tv/bean/Group.java b/app/src/main/java/com/fongmi/android/tv/bean/Group.java index 67928d619..83051eaa0 100644 --- a/app/src/main/java/com/fongmi/android/tv/bean/Group.java +++ b/app/src/main/java/com/fongmi/android/tv/bean/Group.java @@ -20,7 +20,8 @@ public class Group { private String name; @SerializedName("pass") private String pass; - private boolean select; + + private boolean activated; private int position; public Group(String name) { @@ -60,12 +61,12 @@ public class Group { this.pass = pass; } - public boolean isSelect() { - return select; + public boolean isActivated() { + return activated; } - public void setSelect(boolean select) { - this.select = select; + public void setActivated(boolean activated) { + this.activated = activated; } public int getPosition() { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 86389e00a..eae9b7749 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -53,7 +53,7 @@ 配置 渲染方式 缩放比例 - 图片品質 + 图片品质 图片尺寸 版本