diff --git a/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/BaseFragment.java b/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/BaseFragment.java index 44f22bf16..e62d2ff34 100644 --- a/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/BaseFragment.java +++ b/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/BaseFragment.java @@ -6,6 +6,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -15,25 +16,32 @@ import androidx.fragment.app.Fragment; */ public abstract class BaseFragment extends Fragment { public Context mContext; - + private View itemView; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View itemView = createView(inflater, container, savedInstanceState); this.mContext = getContext(); + this.itemView = itemView; initView(itemView); return itemView; } + public T findViewById(@IdRes int id) { + return itemView.findViewById(id); + } + /** * 初始化页面 + * * @param itemView 布局view */ public abstract void initView(View itemView); /** * 创建页面 + * * @return 布局 */ public abstract View createView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState); diff --git a/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/GiftDialogFragment.java b/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/GiftDialogFragment.java new file mode 100644 index 000000000..9687b9f8a --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/GiftDialogFragment.java @@ -0,0 +1,41 @@ +package com.shayu.onetoone.activity.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.shayu.onetoone.R; +import com.shayu.onetoone.adapter.GiftListAdapter; +import com.shayu.onetoone.bean.GiftBean; + +import java.util.List; + +import androidx.recyclerview.widget.RecyclerView; + +public class GiftDialogFragment extends BaseFragment { + private RecyclerView recyclerView; + private GiftListAdapter mAdapter; + + List list; + + public GiftDialogFragment() { + } + + public void setList(List list) { + this.list = list; + mAdapter.setList(list); + } + + @Override + public void initView(View itemView) { + recyclerView = findViewById(R.id.recyclerView); + mAdapter = new GiftListAdapter(getContext()); + recyclerView.setAdapter(mAdapter); + } + + @Override + public View createView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_gift, container, false); + } +} diff --git a/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/message/ChatMessageFragment.java b/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/message/ChatMessageFragment.java index 22990decc..f6e9bc859 100644 --- a/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/message/ChatMessageFragment.java +++ b/OneToOne/src/main/java/com/shayu/onetoone/activity/fragments/message/ChatMessageFragment.java @@ -30,6 +30,8 @@ import com.shayu.onetoone.listener.OnDialogClickListener; import com.shayu.onetoone.listener.OnSendMessageListener; import com.shayu.onetoone.manager.OTONetManager; import com.shayu.onetoone.manager.SendMessageManager; +import com.shayu.onetoone.view.MsgInputPanelForAudio; +import com.shayu.onetoone.view.MsgInputPanelForGift; import com.yunbao.common.CommonAppConfig; import com.yunbao.common.glide.ImgLoader; import com.yunbao.common.http.base.HttpCallback; @@ -67,12 +69,6 @@ public class ChatMessageFragment extends AbsConversationFragment { ProcessImageUtil cameraUtil; String targetId; Conversation.ConversationType conversationType = Conversation.ConversationType.PRIVATE; - ViewGroup audioLayout; - ImageView btnAudio; - ImageView btnText; - ImageView btnClose; - private float mLastTouchY; - private boolean mUpDirection; RoundedImageView avatar; @@ -88,9 +84,9 @@ public class ChatMessageFragment extends AbsConversationFragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + targetId = getActivity().getIntent().getStringExtra("targetId"); initBtn(); initChat(); - targetId = getActivity().getIntent().getStringExtra("targetId"); cameraUtil = new ProcessImageUtil(getActivity(), "com.shayu.onetoone.fileprovider"); mRongExtension.setVisibility(View.VISIBLE); @@ -176,114 +172,32 @@ public class ChatMessageFragment extends AbsConversationFragment { }); } - @SuppressLint("ClickableViewAccessibility") + MsgInputPanelForAudio audio; + MsgInputPanelForGift giftPanel; + private void initBtn() { try { Field field = mRongExtension.getInputPanel().getClass().getDeclaredField("mInputPanel"); field.setAccessible(true); mInputPanel = (View) field.get(mRongExtension.getInputPanel()); assert mInputPanel != null; - mSendBtn = mInputPanel.findViewById(R.id.send_btn); - ImageView mVoiceToggleBtn = (ImageView) mInputPanel.findViewById(R.id.input_panel_voice_toggle); + mSendBtn = mInputPanel.getRootView().findViewById(R.id.send_btn); + + img = mInputPanel.getRootView().findViewById(R.id.input_panel_image_btn); + video = mInputPanel.getRootView().findViewById(R.id.input_panel_video_btn); + call = mInputPanel.getRootView().findViewById(R.id.input_panel_call_btn); + gift = mInputPanel.getRootView().findViewById(R.id.input_panel_gift_btn); + mEditText = mInputPanel.getRootView().findViewById(R.id.edit_btn); + ImageView mVoiceToggleBtn = (ImageView) mInputPanel.getRootView().findViewById(R.id.input_panel_voice_toggle); assert mVoiceToggleBtn != null; - - mVoiceToggleBtn.setOnClickListener(v -> { - SendMessageManager.sendMessageForAudio(targetId, new OnSendMessageListener() { - @Override - public void onSuccess(String token) { - super.onSuccess(token); - ChatMessageFragment.this.token = token; - changeAudioLayout(); - } - - @Override - public void onError(int status, String msg) { - super.onError(status, msg); - if (status == OnSendMessageListener.STATUS_NOT_PRICE) { - new TipsDialog(mContext) - .setTitle("餘額不足") - .setContent(String.format("聊天每條續消耗%s鑽,您可通過充值獲取更多鑽石,以便繼續聊天", msg)) - .setOnDialogClickListener(new OnDialogClickListener() { - - @Override - public void onApply(Dialog dialog) { - super.onApply(dialog); - } - }).showDialog(); - } else { - ToastUtil.show(msg); - } - } - }); - + audio = new MsgInputPanelForAudio(targetId, getActivity(), mRongExtension); + giftPanel = new MsgInputPanelForGift(targetId, getActivity(), mRongExtension); + mVoiceToggleBtn.setOnClickListener(view -> { + audio.show(); }); - img = mInputPanel.findViewById(R.id.input_panel_image_btn); - video = mInputPanel.findViewById(R.id.input_panel_video_btn); - call = mInputPanel.findViewById(R.id.input_panel_call_btn); - gift = mInputPanel.findViewById(R.id.input_panel_gift_btn); - audioLayout = mInputPanel.findViewById(R.id.audio_layout); - audioLayout.addView(LayoutInflater.from(mContext).inflate(R.layout.view_message_input_audio, (ViewGroup) mInputPanel,false)); - btnAudio = audioLayout.findViewById(R.id.audio_btn); - btnText = audioLayout.findViewById(R.id.text_btn); - btnClose = audioLayout.findViewById(R.id.close_btn); - mEditText = audioLayout.findViewById(R.id.edit_btn); - btnText.setOnClickListener(v -> changeAudioLayout()); - btnClose.setOnClickListener(v -> changeAudioLayout()); - - btnAudio.setOnTouchListener(new View.OnTouchListener() { - - @Override - public boolean onTouch(View v, MotionEvent event) { - float mOffsetLimit = 70.0F * v.getContext().getResources().getDisplayMetrics().density; - String[] permissions = new String[]{"android.permission.RECORD_AUDIO"}; - if (!PermissionCheckUtil.checkPermissions(v.getContext(), permissions) && event.getAction() == 0) { - PermissionCheckUtil.requestPermissions(ChatMessageFragment.this, permissions, 100); - return true; - } else { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (AudioPlayManager.getInstance().isPlaying()) { - AudioPlayManager.getInstance().stopPlay(); - } - - if (RongOperationPermissionUtils.isOnRequestHardwareResource()) { - Toast.makeText(v.getContext(), v.getContext().getResources().getString(io.rong.imkit.R.string.rc_voip_occupying), Toast.LENGTH_SHORT).show(); - return true; - } - ChatMessageFragment.this.mLastTouchY = event.getY(); - ChatMessageFragment.this.mUpDirection = false; - AudioRecordManager.getInstance().startRecord(v.getRootView(), mRongExtension.getConversationIdentifier()); - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (ChatMessageFragment.this.mLastTouchY - event.getY() > mOffsetLimit && !ChatMessageFragment.this.mUpDirection) { - AudioRecordManager.getInstance().willCancelRecord(); - ChatMessageFragment.this.mUpDirection = true; - } else if (event.getY() - ChatMessageFragment.this.mLastTouchY > -mOffsetLimit && ChatMessageFragment.this.mUpDirection) { - AudioRecordManager.getInstance().continueRecord(); - ChatMessageFragment.this.mUpDirection = false; - } - - - } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { - AudioRecordManager.getInstance().stopRecord(); - SendMessageManager.onCallSuccess(token, new OnSendMessageListener() { - @Override - public void onError(int status, String msg) { - super.onError(status, msg); - ToastUtil.show(msg); - } - }); - } - - if (mRongExtension.getConversationIdentifier().getType().equals(Conversation.ConversationType.PRIVATE)) { - RongIMClient.getInstance().sendTypingStatus(mRongExtension.getConversationIdentifier().getType(), - mRongExtension.getConversationIdentifier().getTargetId(), "RC:VcMsg"); - } - } - return true; - } - }); gift.setOnClickListener(v -> { - new GiftDialog(mContext).showDialog(); + giftPanel.show(); }); } catch (Exception e) { e.printStackTrace(); @@ -291,14 +205,6 @@ public class ChatMessageFragment extends AbsConversationFragment { } } - private void changeAudioLayout() { - if (audioLayout.getVisibility() == View.VISIBLE) { - audioLayout.setVisibility(View.GONE); - SendMessageManager.cancel(token); - } else { - audioLayout.setVisibility(View.VISIBLE); - } - } private void sendText() { Conversation.ConversationType conversationType = Conversation.ConversationType.PRIVATE; diff --git a/OneToOne/src/main/java/com/shayu/onetoone/adapter/GiftListAdapter.java b/OneToOne/src/main/java/com/shayu/onetoone/adapter/GiftListAdapter.java index b9ac260b4..92101b023 100644 --- a/OneToOne/src/main/java/com/shayu/onetoone/adapter/GiftListAdapter.java +++ b/OneToOne/src/main/java/com/shayu/onetoone/adapter/GiftListAdapter.java @@ -14,14 +14,17 @@ import com.shayu.onetoone.R; import com.shayu.onetoone.bean.GiftBean; import com.yunbao.common.glide.ImgLoader; +import java.util.ArrayList; import java.util.List; public class GiftListAdapter extends RecyclerView.Adapter { private Context mContext; private List list; + private int selectPosition = 0; public GiftListAdapter(Context mContext) { this.mContext = mContext; + list = new ArrayList<>(); } public void setList(List list) { @@ -45,7 +48,7 @@ public class GiftListAdapter extends RecyclerView.Adapter { - select.setVisibility(View.VISIBLE); + selectPosition = getAbsoluteAdapterPosition(); + notifyDataSetChanged(); }); } @@ -67,6 +71,12 @@ public class GiftListAdapter extends RecyclerView.Adapter>() { @Override public void onSuccess(List data) { - mAdapter.setList(data); + List list=new ArrayList<>(); + for (int i = 0; i < 10; i++) { + list.addAll(data); + } + mAdapter.setList(list); } @Override diff --git a/OneToOne/src/main/java/com/shayu/onetoone/view/AbsInputPanel.java b/OneToOne/src/main/java/com/shayu/onetoone/view/AbsInputPanel.java new file mode 100644 index 000000000..aeb253cba --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/view/AbsInputPanel.java @@ -0,0 +1,53 @@ +package com.shayu.onetoone.view; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.shayu.onetoone.R; +import com.yunbao.common.utils.ToastUtil; + +import java.lang.reflect.Field; + +import androidx.fragment.app.FragmentActivity; +import io.rong.imkit.conversation.extension.RongExtension; + +public abstract class AbsInputPanel { + FragmentActivity mContext; + RongExtension mRongExtension; + View mInputPanel; + String targetId; + ViewGroup rootLayout; + View rootView; + + public AbsInputPanel(String targetId, FragmentActivity mContext, RongExtension mRongExtension, int layout) { + this.mContext = mContext; + this.mRongExtension = mRongExtension; + this.targetId = targetId; + try { + Field field = mRongExtension.getInputPanel().getClass().getDeclaredField("mInputPanel"); + field.setAccessible(true); + mInputPanel = (View) field.get(mRongExtension.getInputPanel()); + rootLayout = mInputPanel.findViewById(R.id.audio_layout); + assert rootLayout != null; + rootView = LayoutInflater.from(mContext).inflate(layout, mRongExtension, false); + init(rootView); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public int show() { + if (rootLayout.getVisibility() == View.VISIBLE) { + rootLayout.setVisibility(View.GONE); + return View.GONE; + } + rootLayout.removeAllViews(); + rootLayout.addView(rootView); + + rootLayout.setVisibility(View.VISIBLE); + return View.VISIBLE; + } + + public abstract void init(View viewGroup); +} diff --git a/OneToOne/src/main/java/com/shayu/onetoone/view/MsgInputPanelForAudio.java b/OneToOne/src/main/java/com/shayu/onetoone/view/MsgInputPanelForAudio.java new file mode 100644 index 000000000..17ac31b32 --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/view/MsgInputPanelForAudio.java @@ -0,0 +1,192 @@ +package com.shayu.onetoone.view; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.Toast; + +import com.blankj.utilcode.util.PermissionUtils; +import com.shayu.onetoone.R; +import com.shayu.onetoone.dialog.TipsDialog; +import com.shayu.onetoone.listener.OnDialogClickListener; +import com.shayu.onetoone.listener.OnSendMessageListener; +import com.shayu.onetoone.manager.SendMessageManager; +import com.yunbao.common.utils.ToastUtil; + +import java.lang.reflect.Field; + +import androidx.fragment.app.FragmentActivity; +import io.rong.imkit.conversation.extension.RongExtension; +import io.rong.imkit.manager.AudioPlayManager; +import io.rong.imkit.manager.AudioRecordManager; +import io.rong.imkit.utils.PermissionCheckUtil; +import io.rong.imkit.utils.RongOperationPermissionUtils; +import io.rong.imlib.RongIMClient; +import io.rong.imlib.model.Conversation; + +public class MsgInputPanelForAudio extends AbsInputPanel { + ImageView btnAudio; + ImageView btnText; + ImageView btnClose; + private float mLastTouchY; + private boolean mUpDirection; + + private String token; + + + OnSendMessageListener listener; + + public MsgInputPanelForAudio(String targetId, FragmentActivity mContext, RongExtension mRongExtension) { + super(targetId, mContext, mRongExtension, R.layout.view_message_input_audio); + } + + public void setOnSendMessageListener(OnSendMessageListener listener) { + this.listener = listener; + } + + @Override + public int show() { + if (super.show() == View.GONE) { + SendMessageManager.cancel(token); + return View.GONE; + } + return View.VISIBLE; + } + + private boolean isAudio; + + @SuppressLint("ClickableViewAccessibility") + @Override + public void init(View viewGroup) { + + btnAudio = viewGroup.findViewById(R.id.audio_btn); + btnText = viewGroup.findViewById(R.id.text_btn); + btnClose = viewGroup.findViewById(R.id.close_btn); + + btnAudio.setOnTouchListener((v, event) -> { + System.out.println("点击事件:" + event.getAction()+" isAudio = "+isAudio); + float mOffsetLimit = 70.0F * v.getContext().getResources().getDisplayMetrics().density; + String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO}; + if (!PermissionCheckUtil.checkPermissions(v.getContext(), permissions) && event.getAction() == 0) { + PermissionCheckUtil.requestPermissions(mContext, permissions, 100); + PermissionUtils.permission(Manifest.permission.RECORD_AUDIO).request(); + System.out.println("点击事件 没权限掉了"); + return true; + } else { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + System.out.println("点击事件:点下 isAudio = "+isAudio); + if (isAudio) { + if (onDown(v, event.getY())) { + System.out.println("点击事件:内部点击,返回掉了"); + return true; + } + } else { + System.out.println("点击事件,准备进token"); + toToken(v, event.getY()); + } + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (isAudio) { + onMove(v, event, mOffsetLimit); + } + + } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { + if (isAudio) { + onUp(v, event); + } + } + if (isAudio) { + if (mRongExtension.getConversationIdentifier().getType().equals(Conversation.ConversationType.PRIVATE)) { + RongIMClient.getInstance().sendTypingStatus(mRongExtension.getConversationIdentifier().getType(), + mRongExtension.getConversationIdentifier().getTargetId(), "RC:VcMsg"); + } + } + } + return true; + }); + + } + + private void toToken(View v, float mLastTouchY) { + System.out.println("点击事件:调用token"); + SendMessageManager.sendMessageForAudio(targetId, new OnSendMessageListener() { + @Override + public void onSuccess(String token) { + super.onSuccess(token); + MsgInputPanelForAudio.this.token = token; + isAudio = true; + onDown(v, mLastTouchY); + } + + @Override + public void onError(int status, String msg) { + super.onError(status, msg); + if (status == OnSendMessageListener.STATUS_NOT_PRICE) { + new TipsDialog(mContext) + .setTitle("餘額不足") + .setContent(String.format("聊天每條續消耗%s鑽,您可通過充值獲取更多鑽石,以便繼續聊天", msg)) + .setOnDialogClickListener(new OnDialogClickListener() { + + @Override + public void onApply(Dialog dialog) { + super.onApply(dialog); + } + }).showDialog(); + } else { + ToastUtil.show(msg); + } + } + }); + + } + + private boolean onDown(View v, float mLastTouchY) { + System.out.println("点击事件 mLastTouchY = "+mLastTouchY); + if (AudioPlayManager.getInstance().isPlaying()) { + AudioPlayManager.getInstance().stopPlay(); + } + + if (RongOperationPermissionUtils.isOnRequestHardwareResource()) { + Toast.makeText(v.getContext(), v.getContext().getResources().getString(io.rong.imkit.R.string.rc_voip_occupying), Toast.LENGTH_SHORT).show(); + return true; + } + MsgInputPanelForAudio.this.mLastTouchY = mLastTouchY; + MsgInputPanelForAudio.this.mUpDirection = false; + AudioRecordManager.getInstance().startRecord(v.getRootView(), mRongExtension.getConversationIdentifier()); + return false; + } + + private boolean onMove(View v, MotionEvent event, float mOffsetLimit) { + if (MsgInputPanelForAudio.this.mLastTouchY - event.getY() > mOffsetLimit && !MsgInputPanelForAudio.this.mUpDirection) { + AudioRecordManager.getInstance().willCancelRecord(); + MsgInputPanelForAudio.this.mUpDirection = true; + } else if (event.getY() - MsgInputPanelForAudio.this.mLastTouchY > -mOffsetLimit && MsgInputPanelForAudio.this.mUpDirection) { + AudioRecordManager.getInstance().continueRecord(); + MsgInputPanelForAudio.this.mUpDirection = false; + } + return false; + } + + private boolean onUp(View v, MotionEvent event) { + AudioRecordManager.getInstance().stopRecord(); + if (mUpDirection) { + ToastUtil.show("取消发送"); + SendMessageManager.cancel(token); + isAudio = false; + return false; + } + SendMessageManager.onCallSuccess(token, new OnSendMessageListener() { + @Override + public void onError(int status, String msg) { + super.onError(status, msg); + ToastUtil.show(msg); + isAudio = false; + } + }); + return false; + } +} diff --git a/OneToOne/src/main/java/com/shayu/onetoone/view/MsgInputPanelForGift.java b/OneToOne/src/main/java/com/shayu/onetoone/view/MsgInputPanelForGift.java new file mode 100644 index 000000000..2cf815671 --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/view/MsgInputPanelForGift.java @@ -0,0 +1,75 @@ +package com.shayu.onetoone.view; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import com.shayu.onetoone.R; +import com.shayu.onetoone.adapter.GiftListAdapter; +import com.shayu.onetoone.bean.GiftBean; +import com.shayu.onetoone.manager.OTONetManager; +import com.shayu.onetoone.widget.PagerConfig; +import com.shayu.onetoone.widget.PagerGridLayoutManager; +import com.shayu.onetoone.widget.PagerGridSnapHelper; +import com.yunbao.common.http.base.HttpCallback; +import com.yunbao.common.utils.ToastUtil; + +import java.util.ArrayList; +import java.util.List; + +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; +import io.rong.imkit.conversation.extension.RongExtension; + +public class MsgInputPanelForGift extends AbsInputPanel{ + RecyclerView gifList; + GiftListAdapter mAdapter; + TextView money; + Button topUpBtn; + Button sendBtn; + + public MsgInputPanelForGift(String targetId, FragmentActivity fragmentActivity, RongExtension mRongExtension) { + super(targetId, fragmentActivity, mRongExtension, R.layout.view_message_input_gift); + } + + @Override + public int show() { + return super.show(); + } + + @Override + public void init(View viewGroup) { + gifList = viewGroup.findViewById(R.id.gift_list); + money = viewGroup.findViewById(R.id.money); + topUpBtn = viewGroup.findViewById(R.id.top_up_btn); + sendBtn =viewGroup.findViewById(R.id.send_btn); + mAdapter = new GiftListAdapter(mContext); + PagerGridLayoutManager manager = new PagerGridLayoutManager(2, 4, PagerGridLayoutManager.HORIZONTAL); + gifList.setLayoutManager(manager); + PagerGridSnapHelper pageSnapHelper = new PagerGridSnapHelper(); + pageSnapHelper.attachToRecyclerView(gifList); + manager.setAllowContinuousScroll(false); + PagerConfig.setMillisecondsPreInch(150); + gifList.setAdapter(mAdapter); + initData(); + } + private void initData() { + OTONetManager.getInstance(mContext) + .getGiftList(new HttpCallback>() { + @Override + public void onSuccess(List data) { + List list=new ArrayList<>(); + for (int i = 0; i < 10; i++) { + list.addAll(data); + } + mAdapter.setList(list); + } + + @Override + public void onError(String error) { + + } + }); + } +} diff --git a/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerConfig.java b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerConfig.java new file mode 100644 index 000000000..23f995889 --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerConfig.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 GcsSloop + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Last modified 2017-09-20 16:32:43 + * + * GitHub: https://github.com/GcsSloop + * WeiBo: http://weibo.com/GcsSloop + * WebSite: http://www.gcssloop.com + */ + +package com.shayu.onetoone.widget; + +import android.util.Log; + +/** + * 作用:Pager配置 + * 作者:GcsSloop + * 摘要:主要用于Log的显示与关闭 + */ +public class PagerConfig { + private static final String TAG = "PagerGrid"; + private static boolean sShowLog = false; + private static int sFlingThreshold = 1000; // Fling 阀值,滚动速度超过该阀值才会触发滚动 + private static float sMillisecondsPreInch = 60f; // 每一个英寸滚动需要的微秒数,数值越大,速度越慢 + + /** + * 判断是否输出日志 + * + * @return true 输出,false 不输出 + */ + public static boolean isShowLog() { + return sShowLog; + } + + /** + * 设置是否输出日志 + * + * @param showLog 是否输出 + */ + public static void setShowLog(boolean showLog) { + sShowLog = showLog; + } + + /** + * 获取当前滚动速度阀值 + * + * @return 当前滚动速度阀值 + */ + public static int getFlingThreshold() { + return sFlingThreshold; + } + + /** + * 设置当前滚动速度阀值 + * + * @param flingThreshold 滚动速度阀值 + */ + public static void setFlingThreshold(int flingThreshold) { + sFlingThreshold = flingThreshold; + } + + /** + * 获取滚动速度 英寸/微秒 + * + * @return 英寸滚动速度 + */ + public static float getMillisecondsPreInch() { + return sMillisecondsPreInch; + } + + /** + * 设置像素滚动速度 英寸/微秒 + * + * @param millisecondsPreInch 英寸滚动速度 + */ + public static void setMillisecondsPreInch(float millisecondsPreInch) { + sMillisecondsPreInch = millisecondsPreInch; + } + + //--- 日志 ------------------------------------------------------------------------------------- + + public static void Logi(String msg) { + if (!PagerConfig.isShowLog()) return; + Log.i(TAG, msg); + } + + public static void Loge(String msg) { + if (!PagerConfig.isShowLog()) return; + Log.e(TAG, msg); + } +} diff --git a/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridLayoutManager.java b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridLayoutManager.java new file mode 100644 index 000000000..2a726585f --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridLayoutManager.java @@ -0,0 +1,908 @@ +/* + * Copyright 2017 GcsSloop + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Last modified 2017-09-20 16:32:43 + * + * GitHub: https://github.com/GcsSloop + * WeiBo: http://weibo.com/GcsSloop + * WebSite: http://www.gcssloop.com + */ + +package com.shayu.onetoone.widget; + +import android.annotation.SuppressLint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; + +import static android.view.View.MeasureSpec.EXACTLY; +import static com.shayu.onetoone.widget.PagerConfig.Loge; +import static com.shayu.onetoone.widget.PagerConfig.Logi; +import static com.yunbao.common.views.weight.VerticalViewPager.SCROLL_STATE_IDLE; + + +/** + * 作用:分页的网格布局管理器 + * 作者:GcsSloop + * 摘要: + * 1. 网格布局 + * 2. 支持水平分页和垂直分页 + * 3. 杜绝高内存占用 + */ +public class PagerGridLayoutManager extends RecyclerView.LayoutManager + implements RecyclerView.SmoothScroller.ScrollVectorProvider { + private static final String TAG = PagerGridLayoutManager.class.getSimpleName(); + + public static final int VERTICAL = 0; // 垂直滚动 + public static final int HORIZONTAL = 1; // 水平滚动 + + @IntDef({VERTICAL, HORIZONTAL}) + public @interface OrientationType {} // 滚动类型 + + @OrientationType + private int mOrientation; // 默认水平滚动 + + private int mOffsetX = 0; // 水平滚动距离(偏移量) + private int mOffsetY = 0; // 垂直滚动距离(偏移量) + + private int mRows; // 行数 + private int mColumns; // 列数 + private int mOnePageSize; // 一页的条目数量 + + private SparseArray mItemFrames; // 条目的显示区域 + + private int mItemWidth = 0; // 条目宽度 + private int mItemHeight = 0; // 条目高度 + + private int mWidthUsed = 0; // 已经使用空间,用于测量View + private int mHeightUsed = 0; // 已经使用空间,用于测量View + + private int mMaxScrollX; // 最大允许滑动的宽度 + private int mMaxScrollY; // 最大允许滑动的高度 + private int mScrollState = SCROLL_STATE_IDLE; // 滚动状态 + + private boolean mAllowContinuousScroll = true; // 是否允许连续滚动 + + private RecyclerView mRecyclerView; + + /** + * 构造函数 + * + * @param rows 行数 + * @param columns 列数 + * @param orientation 方向 + */ + public PagerGridLayoutManager(@IntRange(from = 1, to = 100) int rows, + @IntRange(from = 1, to = 100) int columns, + @OrientationType int orientation) { + mItemFrames = new SparseArray<>(); + mOrientation = orientation; + mRows = rows; + mColumns = columns; + mOnePageSize = mRows * mColumns; + } + + @Override + public void onAttachedToWindow(RecyclerView view) { + super.onAttachedToWindow(view); + mRecyclerView = view; + } + + //--- 处理布局 ---------------------------------------------------------------------------------- + + /** + * 布局子View + * + * @param recycler Recycler + * @param state State + */ + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + Logi("Item onLayoutChildren"); + Logi("Item onLayoutChildren isPreLayout = " + state.isPreLayout()); + Logi("Item onLayoutChildren isMeasuring = " + state.isMeasuring()); + Loge("Item onLayoutChildren state = " + state); + + // 如果是 preLayout 则不重新布局 + if (state.isPreLayout() || !state.didStructureChange()) { + return; + } + + if (getItemCount() == 0) { + removeAndRecycleAllViews(recycler); + // 页面变化回调 + setPageCount(0); + setPageIndex(0, false); + return; + } else { + setPageCount(getTotalPageCount()); + setPageIndex(getPageIndexByOffset(), false); + } + + // 计算页面数量 + int mPageCount = getItemCount() / mOnePageSize; + if (getItemCount() % mOnePageSize != 0) { + mPageCount++; + } + + // 计算可以滚动的最大数值,并对滚动距离进行修正 + if (canScrollHorizontally()) { + mMaxScrollX = (mPageCount - 1) * getUsableWidth(); + mMaxScrollY = 0; + if (mOffsetX > mMaxScrollX) { + mOffsetX = mMaxScrollX; + } + } else { + mMaxScrollX = 0; + mMaxScrollY = (mPageCount - 1) * getUsableHeight(); + if (mOffsetY > mMaxScrollY) { + mOffsetY = mMaxScrollY; + } + } + + // 接口回调 + // setPageCount(mPageCount); + // setPageIndex(mCurrentPageIndex, false); + + Logi("count = " + getItemCount()); + + if (mItemWidth <= 0) { + mItemWidth = getUsableWidth() / mColumns; + } + if (mItemHeight <= 0) { + mItemHeight = getUsableHeight() / mRows; + } + + mWidthUsed = getUsableWidth() - mItemWidth; + mHeightUsed = getUsableHeight() - mItemHeight; + + // 预存储两页的View显示区域 + for (int i = 0; i < mOnePageSize * 2; i++) { + getItemFrameByPosition(i); + } + + if (mOffsetX == 0 && mOffsetY == 0) { + // 预存储View + for (int i = 0; i < mOnePageSize; i++) { + if (i >= getItemCount()) break; // 防止数据过少时导致数组越界异常 + View view = recycler.getViewForPosition(i); + addView(view); + measureChildWithMargins(view, mWidthUsed, mHeightUsed); + } + } + + // 回收和填充布局 + recycleAndFillItems(recycler, state, true); + } + + /** + * 布局结束 + * + * @param state State + */ + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + if (state.isPreLayout()) return; + // 页面状态回调 + setPageCount(getTotalPageCount()); + setPageIndex(getPageIndexByOffset(), false); + } + + /** + * 回收和填充布局 + * + * @param recycler Recycler + * @param state State + * @param isStart 是否从头开始,用于控制View遍历方向,true 为从头到尾,false 为从尾到头 + */ + @SuppressLint("CheckResult") + private void recycleAndFillItems(RecyclerView.Recycler recycler, RecyclerView.State state, + boolean isStart) { + if (state.isPreLayout()) { + return; + } + + Logi("mOffsetX = " + mOffsetX); + Logi("mOffsetY = " + mOffsetY); + + // 计算显示区域区前后多存储一列或则一行 + Rect displayRect = new Rect(mOffsetX - mItemWidth, mOffsetY - mItemHeight, + getUsableWidth() + mOffsetX + mItemWidth, getUsableHeight() + mOffsetY + mItemHeight); + // 对显显示区域进行修正(计算当前显示区域和最大显示区域对交集) + displayRect.intersect(0, 0, mMaxScrollX + getUsableWidth(), mMaxScrollY + getUsableHeight()); + Loge("displayRect = " + displayRect.toString()); + + int startPos = 0; // 获取第一个条目的Pos + int pageIndex = getPageIndexByOffset(); + startPos = pageIndex * mOnePageSize; + Logi("startPos = " + startPos); + startPos = startPos - mOnePageSize * 2; + if (startPos < 0) { + startPos = 0; + } + int stopPos = startPos + mOnePageSize * 4; + if (stopPos > getItemCount()) { + stopPos = getItemCount(); + } + + Loge("startPos = " + startPos); + Loge("stopPos = " + stopPos); + + detachAndScrapAttachedViews(recycler); // 移除所有View + + if (isStart) { + for (int i = startPos; i < stopPos; i++) { + addOrRemove(recycler, displayRect, i); + } + } else { + for (int i = stopPos - 1; i >= startPos; i--) { + addOrRemove(recycler, displayRect, i); + } + } + Loge("child count = " + getChildCount()); + } + + /** + * 添加或者移除条目 + * + * @param recycler RecyclerView + * @param displayRect 显示区域 + * @param i 条目下标 + */ + private void addOrRemove(RecyclerView.Recycler recycler, Rect displayRect, int i) { + View child = recycler.getViewForPosition(i); + Rect rect = getItemFrameByPosition(i); + if (!Rect.intersects(displayRect, rect)) { + removeAndRecycleView(child, recycler); // 回收入暂存区 + } else { + addView(child); + measureChildWithMargins(child, mWidthUsed, mHeightUsed); + RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); + layoutDecorated(child, + rect.left - mOffsetX + lp.leftMargin + getPaddingLeft(), + rect.top - mOffsetY + lp.topMargin + getPaddingTop(), + rect.right - mOffsetX - lp.rightMargin + getPaddingLeft(), + rect.bottom - mOffsetY - lp.bottomMargin + getPaddingTop()); + } + } + + + //--- 处理滚动 ---------------------------------------------------------------------------------- + + /** + * 水平滚动 + * + * @param dx 滚动距离 + * @param recycler 回收器 + * @param state 滚动状态 + * @return 实际滚动距离 + */ + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State + state) { + int newX = mOffsetX + dx; + int result = dx; + if (newX > mMaxScrollX) { + result = mMaxScrollX - mOffsetX; + } else if (newX < 0) { + result = 0 - mOffsetX; + } + mOffsetX += result; + setPageIndex(getPageIndexByOffset(), true); + offsetChildrenHorizontal(-result); + if (result > 0) { + recycleAndFillItems(recycler, state, true); + } else { + recycleAndFillItems(recycler, state, false); + } + return result; + } + + /** + * 垂直滚动 + * + * @param dy 滚动距离 + * @param recycler 回收器 + * @param state 滚动状态 + * @return 实际滚动距离 + */ + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State + state) { + int newY = mOffsetY + dy; + int result = dy; + if (newY > mMaxScrollY) { + result = mMaxScrollY - mOffsetY; + } else if (newY < 0) { + result = 0 - mOffsetY; + } + mOffsetY += result; + setPageIndex(getPageIndexByOffset(), true); + offsetChildrenVertical(-result); + if (result > 0) { + recycleAndFillItems(recycler, state, true); + } else { + recycleAndFillItems(recycler, state, false); + } + return result; + } + + /** + * 监听滚动状态,滚动结束后通知当前选中的页面 + * + * @param state 滚动状态 + */ + @Override + public void onScrollStateChanged(int state) { + Logi("onScrollStateChanged = " + state); + mScrollState = state; + super.onScrollStateChanged(state); + if (state == SCROLL_STATE_IDLE) { + setPageIndex(getPageIndexByOffset(), false); + } + } + + + //--- 私有方法 ---------------------------------------------------------------------------------- + + /** + * 获取条目显示区域 + * + * @param pos 位置下标 + * @return 显示区域 + */ + private Rect getItemFrameByPosition(int pos) { + Rect rect = mItemFrames.get(pos); + if (null == rect) { + rect = new Rect(); + // 计算显示区域 Rect + + // 1. 获取当前View所在页数 + int page = pos / mOnePageSize; + + // 2. 计算当前页数左上角的总偏移量 + int offsetX = 0; + int offsetY = 0; + if (canScrollHorizontally()) { + offsetX += getUsableWidth() * page; + } else { + offsetY += getUsableHeight() * page; + } + + // 3. 根据在当前页面中的位置确定具体偏移量 + int pagePos = pos % mOnePageSize; // 在当前页面中是第几个 + int row = pagePos / mColumns; // 获取所在行 + int col = pagePos - (row * mColumns); // 获取所在列 + + offsetX += col * mItemWidth; + offsetY += row * mItemHeight; + + // 状态输出,用于调试 + Logi("pagePos = " + pagePos); + Logi("行 = " + row); + Logi("列 = " + col); + + Logi("offsetX = " + offsetX); + Logi("offsetY = " + offsetY); + + rect.left = offsetX; + rect.top = offsetY; + rect.right = offsetX + mItemWidth; + rect.bottom = offsetY + mItemHeight; + + // 存储 + mItemFrames.put(pos, rect); + } + return rect; + } + + /** + * 获取可用的宽度 + * + * @return 宽度 - padding + */ + private int getUsableWidth() { + return getWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * 获取可用的高度 + * + * @return 高度 - padding + */ + private int getUsableHeight() { + return getHeight() - getPaddingTop() - getPaddingBottom(); + } + + + //--- 页面相关(私有) ----------------------------------------------------------------------------- + + /** + * 获取总页数 + */ + private int getTotalPageCount() { + if (getItemCount() <= 0) return 0; + int totalCount = getItemCount() / mOnePageSize; + if (getItemCount() % mOnePageSize != 0) { + totalCount++; + } + return totalCount; + } + + /** + * 根据pos,获取该View所在的页面 + * + * @param pos position + * @return 页面的页码 + */ + private int getPageIndexByPos(int pos) { + return pos / mOnePageSize; + } + + /** + * 根据 offset 获取页面Index + * + * @return 页面 Index + */ + private int getPageIndexByOffset() { + int pageIndex; + if (canScrollVertically()) { + int pageHeight = getUsableHeight(); + if (mOffsetY <= 0 || pageHeight <= 0) { + pageIndex = 0; + } else { + pageIndex = mOffsetY / pageHeight; + if (mOffsetY % pageHeight > pageHeight / 2) { + pageIndex++; + } + } + } else { + int pageWidth = getUsableWidth(); + if (mOffsetX <= 0 || pageWidth <= 0) { + pageIndex = 0; + } else { + pageIndex = mOffsetX / pageWidth; + if (mOffsetX % pageWidth > pageWidth / 2) { + pageIndex++; + } + } + } + Logi("getPageIndexByOffset pageIndex = " + pageIndex); + return pageIndex; + } + + + //--- 公开方法 ---------------------------------------------------------------------------------- + + /** + * 创建默认布局参数 + * + * @return 默认布局参数 + */ + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + /** + * 处理测量逻辑 + * + * @param recycler RecyclerView + * @param state 状态 + * @param widthMeasureSpec 宽度属性 + * @param heightMeasureSpec 高估属性 + */ + @Override + public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(recycler, state, widthMeasureSpec, heightMeasureSpec); + int widthsize = View.MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值 + int widthmode = View.MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式 + + int heightsize = View.MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值 + int heightmode = View.MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式 + + // 将 wrap_content 转换为 match_parent + if (widthmode != EXACTLY && widthsize > 0) { + widthmode = EXACTLY; + } + if (heightmode != EXACTLY && heightsize > 0) { + heightmode = EXACTLY; + } + setMeasuredDimension(View.MeasureSpec.makeMeasureSpec(widthsize, widthmode), + View.MeasureSpec.makeMeasureSpec(heightsize, heightmode)); + } + + /** + * 是否可以水平滚动 + * + * @return true 是,false 不是。 + */ + @Override + public boolean canScrollHorizontally() { + return mOrientation == HORIZONTAL; + } + + /** + * 是否可以垂直滚动 + * + * @return true 是,false 不是。 + */ + @Override + public boolean canScrollVertically() { + return mOrientation == VERTICAL; + } + + /** + * 找到下一页第一个条目的位置 + * + * @return 第一个搞条目的位置 + */ + int findNextPageFirstPos() { + int page = mLastPageIndex; + page++; + if (page >= getTotalPageCount()) { + page = getTotalPageCount() - 1; + } + Loge("computeScrollVectorForPosition next = " + page); + return page * mOnePageSize; + } + + /** + * 找到上一页的第一个条目的位置 + * + * @return 第一个条目的位置 + */ + int findPrePageFirstPos() { + // 在获取时由于前一页的View预加载出来了,所以获取到的直接就是前一页 + int page = mLastPageIndex; + page--; + Loge("computeScrollVectorForPosition pre = " + page); + if (page < 0) { + page = 0; + } + Loge("computeScrollVectorForPosition pre = " + page); + return page * mOnePageSize; + } + + /** + * 获取当前 X 轴偏移量 + * + * @return X 轴偏移量 + */ + public int getOffsetX() { + return mOffsetX; + } + + /** + * 获取当前 Y 轴偏移量 + * + * @return Y 轴偏移量 + */ + public int getOffsetY() { + return mOffsetY; + } + + + //--- 页面对齐 ---------------------------------------------------------------------------------- + + /** + * 计算到目标位置需要滚动的距离{@link RecyclerView.SmoothScroller.ScrollVectorProvider} + * + * @param targetPosition 目标控件 + * @return 需要滚动的距离 + */ + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + PointF vector = new PointF(); + int[] pos = getSnapOffset(targetPosition); + vector.x = pos[0]; + vector.y = pos[1]; + return vector; + } + + /** + * 获取偏移量(为PagerGridSnapHelper准备) + * 用于分页滚动,确定需要滚动的距离。 + * {@link PagerGridSnapHelper} + * + * @param targetPosition 条目下标 + */ + int[] getSnapOffset(int targetPosition) { + int[] offset = new int[2]; + int[] pos = getPageLeftTopByPosition(targetPosition); + offset[0] = pos[0] - mOffsetX; + offset[1] = pos[1] - mOffsetY; + return offset; + } + + /** + * 根据条目下标获取该条目所在页面的左上角位置 + * + * @param pos 条目下标 + * @return 左上角位置 + */ + private int[] getPageLeftTopByPosition(int pos) { + int[] leftTop = new int[2]; + int page = getPageIndexByPos(pos); + if (canScrollHorizontally()) { + leftTop[0] = page * getUsableWidth(); + leftTop[1] = 0; + } else { + leftTop[0] = 0; + leftTop[1] = page * getUsableHeight(); + } + return leftTop; + } + + /** + * 获取需要对齐的View + * + * @return 需要对齐的View + */ + public View findSnapView() { + if (null != getFocusedChild()) { + return getFocusedChild(); + } + if (getChildCount() <= 0) { + return null; + } + int targetPos = getPageIndexByOffset() * mOnePageSize; // 目标Pos + for (int i = 0; i < getChildCount(); i++) { + int childPos = getPosition(getChildAt(i)); + if (childPos == targetPos) { + return getChildAt(i); + } + } + return getChildAt(0); + } + + + //--- 处理页码变化 ------------------------------------------------------------------------------- + + private boolean mChangeSelectInScrolling = true; // 是否在滚动过程中对页面变化回调 + private int mLastPageCount = -1; // 上次页面总数 + private int mLastPageIndex = -1; // 上次页面下标 + + /** + * 设置页面总数 + * + * @param pageCount 页面总数 + */ + private void setPageCount(int pageCount) { + if (pageCount >= 0) { + if (mPageListener != null && pageCount != mLastPageCount) { + mPageListener.onPageSizeChanged(pageCount); + } + mLastPageCount = pageCount; + } + } + + /** + * 设置当前选中页面 + * + * @param pageIndex 页面下标 + * @param isScrolling 是否处于滚动状态 + */ + private void setPageIndex(int pageIndex, boolean isScrolling) { + Loge("setPageIndex = " + pageIndex + ":" + isScrolling); + if (pageIndex == mLastPageIndex) return; + // 如果允许连续滚动,那么在滚动过程中就会更新页码记录 + if (isAllowContinuousScroll()) { + mLastPageIndex = pageIndex; + } else { + // 否则,只有等滚动停下时才会更新页码记录 + if (!isScrolling) { + mLastPageIndex = pageIndex; + } + } + if (isScrolling && !mChangeSelectInScrolling) return; + if (pageIndex >= 0) { + if (null != mPageListener) { + mPageListener.onPageSelect(pageIndex); + } + } + } + + /** + * 设置是否在滚动状态更新选中页码 + * + * @param changeSelectInScrolling true:更新、false:不更新 + */ + public void setChangeSelectInScrolling(boolean changeSelectInScrolling) { + mChangeSelectInScrolling = changeSelectInScrolling; + } + + /** + * 设置滚动方向 + * + * @param orientation 滚动方向 + * @return 最终的滚动方向 + */ + @OrientationType + public int setOrientationType(@OrientationType int orientation) { + if (mOrientation == orientation || mScrollState != SCROLL_STATE_IDLE) return mOrientation; + mOrientation = orientation; + mItemFrames.clear(); + int x = mOffsetX; + int y = mOffsetY; + mOffsetX = y / getUsableHeight() * getUsableWidth(); + mOffsetY = x / getUsableWidth() * getUsableHeight(); + int mx = mMaxScrollX; + int my = mMaxScrollY; + mMaxScrollX = my / getUsableHeight() * getUsableWidth(); + mMaxScrollY = mx / getUsableWidth() * getUsableHeight(); + return mOrientation; + } + + //--- 滚动到指定位置 ----------------------------------------------------------------------------- + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { + int targetPageIndex = getPageIndexByPos(position); + smoothScrollToPage(targetPageIndex); + } + + /** + * 平滑滚动到上一页 + */ + public void smoothPrePage() { + smoothScrollToPage(getPageIndexByOffset() - 1); + } + + /** + * 平滑滚动到下一页 + */ + public void smoothNextPage() { + smoothScrollToPage(getPageIndexByOffset() + 1); + } + + /** + * 平滑滚动到指定页面 + * + * @param pageIndex 页面下标 + */ + public void smoothScrollToPage(int pageIndex) { + if (pageIndex < 0 || pageIndex >= mLastPageCount) { + Log.e(TAG, "pageIndex is outOfIndex, must in [0, " + mLastPageCount + ")."); + return; + } + if (null == mRecyclerView) { + Log.e(TAG, "RecyclerView Not Found!"); + return; + } + + // 如果滚动到页面之间距离过大,先直接滚动到目标页面到临近页面,在使用 smoothScroll 最终滚动到目标 + // 否则在滚动距离很大时,会导致滚动耗费的时间非常长 + int currentPageIndex = getPageIndexByOffset(); + if (Math.abs(pageIndex - currentPageIndex) > 3) { + if (pageIndex > currentPageIndex) { + scrollToPage(pageIndex - 3); + } else if (pageIndex < currentPageIndex) { + scrollToPage(pageIndex + 3); + } + } + + // 具体执行滚动 + LinearSmoothScroller smoothScroller = new PagerGridSmoothScroller(mRecyclerView); + int position = pageIndex * mOnePageSize; + smoothScroller.setTargetPosition(position); + startSmoothScroll(smoothScroller); + } + + //=== 直接滚动 === + + @Override + public void scrollToPosition(int position) { + int pageIndex = getPageIndexByPos(position); + scrollToPage(pageIndex); + } + + /** + * 上一页 + */ + public void prePage() { + scrollToPage(getPageIndexByOffset() - 1); + } + + /** + * 下一页 + */ + public void nextPage() { + scrollToPage(getPageIndexByOffset() + 1); + } + + /** + * 滚动到指定页面 + * + * @param pageIndex 页面下标 + */ + public void scrollToPage(int pageIndex) { + if (pageIndex < 0 || pageIndex >= mLastPageCount) { + Log.e(TAG, "pageIndex = " + pageIndex + " is out of bounds, mast in [0, " + mLastPageCount + ")"); + return; + } + + if (null == mRecyclerView) { + Log.e(TAG, "RecyclerView Not Found!"); + return; + } + + int mTargetOffsetXBy = 0; + int mTargetOffsetYBy = 0; + if (canScrollVertically()) { + mTargetOffsetXBy = 0; + mTargetOffsetYBy = pageIndex * getUsableHeight() - mOffsetY; + } else { + mTargetOffsetXBy = pageIndex * getUsableWidth() - mOffsetX; + mTargetOffsetYBy = 0; + } + Loge("mTargetOffsetXBy = " + mTargetOffsetXBy); + Loge("mTargetOffsetYBy = " + mTargetOffsetYBy); + mRecyclerView.scrollBy(mTargetOffsetXBy, mTargetOffsetYBy); + setPageIndex(pageIndex, false); + } + + /** + * 是否允许连续滚动,默认为允许 + * + * @return true 允许, false 不允许 + */ + public boolean isAllowContinuousScroll() { + return mAllowContinuousScroll; + } + + /** + * 设置是否允许连续滚动 + * + * @param allowContinuousScroll true 允许,false 不允许 + */ + public void setAllowContinuousScroll(boolean allowContinuousScroll) { + mAllowContinuousScroll = allowContinuousScroll; + } + + //--- 对外接口 ---------------------------------------------------------------------------------- + + private PageListener mPageListener = null; + + public void setPageListener(PageListener pageListener) { + mPageListener = pageListener; + } + + public interface PageListener { + /** + * 页面总数量变化 + * + * @param pageSize 页面总数 + */ + void onPageSizeChanged(int pageSize); + + /** + * 页面被选中 + * + * @param pageIndex 选中的页面 + */ + void onPageSelect(int pageIndex); + } +} \ No newline at end of file diff --git a/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridSmoothScroller.java b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridSmoothScroller.java new file mode 100644 index 000000000..b78f23cd7 --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridSmoothScroller.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 GcsSloop + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Last modified 2018-04-09 23:56:59 + * + * GitHub: https://github.com/GcsSloop + * WeiBo: http://weibo.com/GcsSloop + * WebSite: http://www.gcssloop.com + */ + +package com.shayu.onetoone.widget; + +import android.util.DisplayMetrics; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; + +import static com.shayu.onetoone.widget.PagerConfig.Logi; + + +/** + * 作用:用于处理平滑滚动 + * 作者:GcsSloop + * 摘要:用于用户手指抬起后页面对齐或者 Fling 事件。 + */ +public class PagerGridSmoothScroller extends LinearSmoothScroller { + private RecyclerView mRecyclerView; + + public PagerGridSmoothScroller(@NonNull RecyclerView recyclerView) { + super(recyclerView.getContext()); + mRecyclerView = recyclerView; + } + + @Override + protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager(); + if (null == manager) return; + if (manager instanceof PagerGridLayoutManager) { + PagerGridLayoutManager layoutManager = (PagerGridLayoutManager) manager; + int pos = mRecyclerView.getChildAdapterPosition(targetView); + int[] snapDistances = layoutManager.getSnapOffset(pos); + final int dx = snapDistances[0]; + final int dy = snapDistances[1]; + Logi("dx = " + dx); + Logi("dy = " + dy); + final int time = calculateTimeForScrolling(Math.max(Math.abs(dx), Math.abs(dy))); + if (time > 0) { + action.update(dx, dy, time, mDecelerateInterpolator); + } + } + } + + @Override + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return PagerConfig.getMillisecondsPreInch() / displayMetrics.densityDpi; + } +} diff --git a/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridSnapHelper.java b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridSnapHelper.java new file mode 100644 index 000000000..6bdb778c3 --- /dev/null +++ b/OneToOne/src/main/java/com/shayu/onetoone/widget/PagerGridSnapHelper.java @@ -0,0 +1,203 @@ +/* + * Copyright 2017 GcsSloop + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Last modified 2017-09-20 16:32:43 + * + * GitHub: https://github.com/GcsSloop + * WeiBo: http://weibo.com/GcsSloop + * WebSite: http://www.gcssloop.com + */ + +package com.shayu.onetoone.widget; + + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SnapHelper; + +import static com.shayu.onetoone.widget.PagerConfig.Loge; + + +/** + * 作用:分页居中工具 + * 作者:GcsSloop + * 摘要:每次只滚动一个页面 + */ +public class PagerGridSnapHelper extends SnapHelper { + private RecyclerView mRecyclerView; // RecyclerView + + /** + * 用于将滚动工具和 Recycler 绑定 + * + * @param recyclerView RecyclerView + * @throws IllegalStateException 状态异常 + */ + @Override + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws + IllegalStateException { + super.attachToRecyclerView(recyclerView); + mRecyclerView = recyclerView; + } + + /** + * 计算需要滚动的向量,用于页面自动回滚对齐 + * + * @param layoutManager 布局管理器 + * @param targetView 目标控件 + * @return 需要滚动的距离 + */ + @Nullable + @Override + public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, + @NonNull View targetView) { + int pos = layoutManager.getPosition(targetView); + Loge("findTargetSnapPosition, pos = " + pos); + int[] offset = new int[2]; + if (layoutManager instanceof PagerGridLayoutManager) { + PagerGridLayoutManager manager = (PagerGridLayoutManager) layoutManager; + offset = manager.getSnapOffset(pos); + } + return offset; + } + + /** + * 获得需要对齐的View,对于分页布局来说,就是页面第一个 + * + * @param layoutManager 布局管理器 + * @return 目标控件 + */ + @Nullable + @Override + public View findSnapView(RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof PagerGridLayoutManager) { + PagerGridLayoutManager manager = (PagerGridLayoutManager) layoutManager; + return manager.findSnapView(); + } + return null; + } + + /** + * 获取目标控件的位置下标 + * (获取滚动后第一个View的下标) + * + * @param layoutManager 布局管理器 + * @param velocityX X 轴滚动速率 + * @param velocityY Y 轴滚动速率 + * @return 目标控件的下标 + */ + @Override + public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, + int velocityX, int velocityY) { + int target = RecyclerView.NO_POSITION; + Loge("findTargetSnapPosition, velocityX = " + velocityX + ", velocityY" + velocityY); + if (null != layoutManager && layoutManager instanceof PagerGridLayoutManager) { + PagerGridLayoutManager manager = (PagerGridLayoutManager) layoutManager; + if (manager.canScrollHorizontally()) { + if (velocityX > PagerConfig.getFlingThreshold()) { + target = manager.findNextPageFirstPos(); + } else if (velocityX < -PagerConfig.getFlingThreshold()) { + target = manager.findPrePageFirstPos(); + } + } else if (manager.canScrollVertically()) { + if (velocityY > PagerConfig.getFlingThreshold()) { + target = manager.findNextPageFirstPos(); + } else if (velocityY < -PagerConfig.getFlingThreshold()) { + target = manager.findPrePageFirstPos(); + } + } + } + Loge("findTargetSnapPosition, target = " + target); + return target; + } + + /** + * 一扔(快速滚动) + * + * @param velocityX X 轴滚动速率 + * @param velocityY Y 轴滚动速率 + * @return 是否消费该事件 + */ + @Override + public boolean onFling(int velocityX, int velocityY) { + RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + if (layoutManager == null) { + return false; + } + RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); + if (adapter == null) { + return false; + } + int minFlingVelocity = PagerConfig.getFlingThreshold(); + Loge("minFlingVelocity = " + minFlingVelocity); + return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) + && snapFromFling(layoutManager, velocityX, velocityY); + } + + /** + * 快速滚动的具体处理方案 + * + * @param layoutManager 布局管理器 + * @param velocityX X 轴滚动速率 + * @param velocityY Y 轴滚动速率 + * @return 是否消费该事件 + */ + private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX, + int velocityY) { + if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { + return false; + } + + RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager); + if (smoothScroller == null) { + return false; + } + + int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY); + if (targetPosition == RecyclerView.NO_POSITION) { + return false; + } + + smoothScroller.setTargetPosition(targetPosition); + layoutManager.startSmoothScroll(smoothScroller); + return true; + } + + /** + * 通过自定义 LinearSmoothScroller 来控制速度 + * + * @param layoutManager 布局故哪里去 + * @return 自定义 LinearSmoothScroller + */ + protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) { + if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { + return null; + } + return new PagerGridSmoothScroller(mRecyclerView); + } + + //--- 公开方法 ---------------------------------------------------------------------------------- + + /** + * 设置滚动阀值 + * @param threshold 滚动阀值 + */ + public void setFlingThreshold(int threshold) { + PagerConfig.setFlingThreshold(threshold); + } +} \ No newline at end of file diff --git a/OneToOne/src/main/res/layout/fragment_gift.xml b/OneToOne/src/main/res/layout/fragment_gift.xml new file mode 100644 index 000000000..b295a5c59 --- /dev/null +++ b/OneToOne/src/main/res/layout/fragment_gift.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/OneToOne/src/main/res/layout/item_gift.xml b/OneToOne/src/main/res/layout/item_gift.xml index b81ebe4f0..9ad957014 100644 --- a/OneToOne/src/main/res/layout/item_gift.xml +++ b/OneToOne/src/main/res/layout/item_gift.xml @@ -17,7 +17,7 @@ - + tools:visibility="visible" + android:orientation="horizontal" /> diff --git a/OneToOne/src/main/res/layout/view_message_input_gift.xml b/OneToOne/src/main/res/layout/view_message_input_gift.xml index 80e5895a7..b0af08d54 100644 --- a/OneToOne/src/main/res/layout/view_message_input_gift.xml +++ b/OneToOne/src/main/res/layout/view_message_input_gift.xml @@ -2,28 +2,34 @@ + android:layout_height="wrap_content"> + app:layout_constraintTop_toTopOf="parent" + app:spanCount="4" + tools:listitem="@layout/item_gift" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/gift_list"> + android:textSize="12sp" />