diff --git a/app/src/main/java/com/ik/mboxlauncher/ui/Launcher.java b/app/src/main/java/com/ik/mboxlauncher/ui/Launcher.java index 776fb9a..34694ef 100644 --- a/app/src/main/java/com/ik/mboxlauncher/ui/Launcher.java +++ b/app/src/main/java/com/ik/mboxlauncher/ui/Launcher.java @@ -65,6 +65,7 @@ import com.ik.mboxlauncher.view.MultiView; import com.ik.mboxlauncher.view.CustomRecyclerView; import com.ik.mboxlauncher.view.SplashView; import com.ik.mboxlauncher.view.TimeTextView; +import com.ik.mboxlauncher.view.TvRecyclerView; import java.util.ArrayList; import java.util.HashMap; @@ -79,7 +80,8 @@ public class Launcher extends FragmentActivity implements SplashView.SplashAdLi private MultiView layout_video,layout_music,layout_primevideo,layout_filemanager,layout_hbomax,layout_setting,layout_youtube,layout_recommend,layout_netflix,layout_chrome; private MultiView layout_miracastreceive; private int[] imageResIds={R.drawable.img_video,R.drawable.img_miracastreceive,R.drawable.img_youtube,R.drawable.img_recommend,R.drawable.img_music,R.drawable.img_netflix,R.drawable.img_primevideo,R.drawable.img_filemanager,R.drawable.img_chrome,R.drawable.img_hbomax,R.drawable.img_setting}; - private CustomRecyclerView gv_shortcut,grid_coustom_apps; + private CustomRecyclerView gv_shortcut; + private TvRecyclerView grid_coustom_apps; private StatusLoader mStatusLoader; private ShortAppInfoAdapter mShortAppInfoAdapter=null; private CustomAppAdapter mCustomAppAdapter=null; @@ -878,7 +880,7 @@ public boolean onGenericMotionEvent(MotionEvent event) { } }); - translateAnimation.setDuration(300); + translateAnimation.setDuration(360); translateAnimation.setFillAfter(true); content_view.startAnimation(translateAnimation); @@ -895,7 +897,7 @@ public boolean onGenericMotionEvent(MotionEvent event) { cuttentModel = MODEL_NORMAL; LogUtils.loge("coustom_view.getHeight():"+coustom_view.getLayoutParams().height); TranslateAnimation translateAnimation = new TranslateAnimation(0.0f, 0.0f,(float)(0 - coustom_view.getLayoutParams().height - gv_shortcut.getHeight()),0.0f); - translateAnimation.setDuration(300); + translateAnimation.setDuration(360); translateAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { @@ -906,6 +908,7 @@ public boolean onGenericMotionEvent(MotionEvent event) { @Override public void onAnimationEnd(Animation animation) { + LogUtils.loge("dismissCustomApp onAnimationEnd()"); coustom_view.setVisibility(View.GONE); gv_shortcut.requestFocus(); diff --git a/app/src/main/java/com/ik/mboxlauncher/ui/adapter/CustomAppAdapter.java b/app/src/main/java/com/ik/mboxlauncher/ui/adapter/CustomAppAdapter.java index ed800b3..d2b4ca5 100644 --- a/app/src/main/java/com/ik/mboxlauncher/ui/adapter/CustomAppAdapter.java +++ b/app/src/main/java/com/ik/mboxlauncher/ui/adapter/CustomAppAdapter.java @@ -112,7 +112,7 @@ public class CustomAppAdapter extends RecyclerView.Adapter viewHolder.itemView.requestFocus()); }else { diff --git a/app/src/main/java/com/ik/mboxlauncher/ui/fragment/CategoryFragment.java b/app/src/main/java/com/ik/mboxlauncher/ui/fragment/CategoryFragment.java index 00c7018..e354fc1 100644 --- a/app/src/main/java/com/ik/mboxlauncher/ui/fragment/CategoryFragment.java +++ b/app/src/main/java/com/ik/mboxlauncher/ui/fragment/CategoryFragment.java @@ -3,6 +3,7 @@ package com.ik.mboxlauncher.ui.fragment; import android.view.KeyEvent; import android.view.View; +import android.view.ViewTreeObserver; import android.view.animation.TranslateAnimation; import android.widget.ImageView; import android.widget.TextView; @@ -25,6 +26,7 @@ import com.ik.mboxlauncher.ui.adapter.VideoAppAdapter; import com.ik.mboxlauncher.ui.adapter.MyAppInfoAdapter; import com.ik.mboxlauncher.ui.base.BaseFragment; import com.ik.mboxlauncher.view.CustomRecyclerViewer; +import com.ik.mboxlauncher.view.TvRecyclerView; import java.util.HashMap; import java.util.List; @@ -34,7 +36,7 @@ public abstract class CategoryFragment extends BaseFragment implements AppnetCal private CategoryAppPresenter mCategoryAppPresenter =null; // private MyAppInfoAdapter mMyAppInfoAdapter = null; - protected RecyclerView grid_coustom_apps; + protected TvRecyclerView grid_coustom_apps; protected CustomRecyclerViewer gv_category_apps; protected ImageView img_logo,img_tab_video, img_tab_recommend, img_tab_apps, img_tab_music, img_tab_local; protected TextView tv_title; @@ -288,7 +290,21 @@ public abstract class CategoryFragment extends BaseFragment implements AppnetCal LogUtils.loge("data size is "+shortcutInfoBeanList.size()); mCustomAppAdapter.addDatas(shortcutInfoBeanList); - + grid_coustom_apps.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + grid_coustom_apps.getViewTreeObserver().removeOnGlobalLayoutListener(this); + grid_coustom_apps.scrollToPosition(0); + grid_coustom_apps.post(()->{ + int firstVisibleItemPosition = ((GridLayoutManager) grid_coustom_apps.getLayoutManager()).findFirstVisibleItemPosition(); + RecyclerView.ViewHolder holderview = grid_coustom_apps.findViewHolderForAdapterPosition(firstVisibleItemPosition); + if(holderview !=null){ + View targetView = holderview.itemView; + targetView.post(() -> targetView.requestFocus()); + } + }); + } + }); }else { LogUtils.loge("no data"); } diff --git a/app/src/main/java/com/ik/mboxlauncher/view/TvRecyclerView.java b/app/src/main/java/com/ik/mboxlauncher/view/TvRecyclerView.java new file mode 100644 index 0000000..b1c4be9 --- /dev/null +++ b/app/src/main/java/com/ik/mboxlauncher/view/TvRecyclerView.java @@ -0,0 +1,686 @@ +package com.ik.mboxlauncher.view; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import com.ik.mboxlauncher.R; + + +/** + * Created by ksl on 2026/1/9. + */ +public class TvRecyclerView extends RecyclerView { + private static final String TAG = "TvRecyclerView"; + + private int position; + private int lastFocusPosition; + //焦点是否居中 + private boolean mSelectedItemCentered; + + private boolean hasResetFocus;//自控焦点 + + private boolean mScrollDirecitonUp;//默认向下滚 + + private int mSelectedItemOffsetStart; + + private int mSelectedItemOffsetEnd; + + //分页的时候使用 + private int mLoadMoreBeforehandCount = 0; + private PressBackListenner mPressbackLister; + + public TvRecyclerView(Context context) { + this(context, null); + } + + public TvRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, -1); + } + + public TvRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + initView(); + initAttr(context, attrs); + } + + private void initView() { + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setHasFixedSize(true); + setWillNotDraw(true); + setOverScrollMode(View.OVER_SCROLL_NEVER); + setChildrenDrawingOrderEnabled(true); + + setClipChildren(false); + setClipToPadding(false); + + setClickable(false); + setFocusable(true); + setFocusableInTouchMode(true); + /** + 防止RecyclerView刷新时焦点不错乱bug的步骤如下: + (1)adapter执行setHasStableIds(true)方法 + (2)重写getItemId()方法,让每个view都有各自的id + (3)RecyclerView的动画必须去掉 + */ + setItemAnimator(null); + } + + private void initAttr(Context context, AttributeSet attrs) { + if (attrs != null) { + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.TvRecyclerView); + /** + * 如果是towWayView的layoutManager + */ + final String name = a.getString(R.styleable.TvRecyclerView_tv_layoutManager); + + + mSelectedItemCentered = a.getBoolean(R.styleable.TvRecyclerView_tv_selectedItemCentered, false); + + mLoadMoreBeforehandCount = a.getInteger(R.styleable.TvRecyclerView_tv_loadMoreBeforehandCount, 0); + + mSelectedItemOffsetStart = a.getDimensionPixelSize(R.styleable.TvRecyclerView_tv_selectedItemOffsetStart, 0); + + mSelectedItemOffsetEnd = a.getDimensionPixelSize(R.styleable.TvRecyclerView_tv_selectedItemOffsetEnd, 0); + + a.recycle(); + } + } + + + private int getFreeWidth() { + return getWidth() - getPaddingLeft() - getPaddingRight(); + } + + private int getFreeHeight() { + return getHeight() - getPaddingTop() - getPaddingBottom(); + } + + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); +// View lastFocusedView = getLayoutManager().findViewByPosition(lastFocusPosition); +// if(lastFocusedView!=null){ +// lastFocusedView.requestFocus(); +// } + if(!gainFocus){ + hasResetFocus=false; + } + } + + @Override + public boolean hasFocus() { + return super.hasFocus(); + } + + @Override + public boolean isInTouchMode() { + // 解决4.4版本抢焦点的问题 + if (Build.VERSION.SDK_INT == 19) { + return !(hasFocus() && !super.isInTouchMode()); + } else { + return super.isInTouchMode(); + } + + //return super.isInTouchMode(); + } + + @Override + public void requestChildFocus(View child, View focused) { + + if (null != child) { + if (mSelectedItemCentered) { + mSelectedItemOffsetStart = !isVertical() ? (getFreeWidth() - child.getWidth()) : (getFreeHeight() - child.getHeight()); + mSelectedItemOffsetStart /= 2; + mSelectedItemOffsetEnd = mSelectedItemOffsetStart; + } + lastFocusPosition=getChildViewHolder(child).getAdapterPosition(); + Log.e(TAG,"lastFocusPosition="+lastFocusPosition); + } + super.requestChildFocus(child, focused); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { + final int parentLeft = getPaddingLeft(); + final int parentRight = getWidth() - getPaddingRight(); + + final int parentTop = getPaddingTop(); + final int parentBottom = getHeight() - getPaddingBottom(); + + final int childLeft = child.getLeft() + rect.left; + final int childTop = child.getTop() + rect.top; + + final int childRight = childLeft + rect.width(); + final int childBottom = childTop + rect.height(); + + final int offScreenLeft = Math.min(0, childLeft - parentLeft - mSelectedItemOffsetStart); + final int offScreenRight = Math.max(0, childRight - parentRight + mSelectedItemOffsetEnd); + + final int offScreenTop = Math.min(0, childTop - parentTop - mSelectedItemOffsetStart); + final int offScreenBottom = Math.max(0, childBottom - parentBottom + mSelectedItemOffsetEnd); + + + final boolean canScrollHorizontal = getLayoutManager().canScrollHorizontally(); + final boolean canScrollVertical = getLayoutManager().canScrollVertically(); + + // Favor the "start" layout direction over the end when bringing one side or the other + // of a large rect into view. If we decide to bring in end because start is already + // visible, limit the scroll such that start won't go out of bounds. + final int dx; + if (canScrollHorizontal) { + if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) { + dx = offScreenRight != 0 ? offScreenRight + : Math.max(offScreenLeft, childRight - parentRight); + } else { + dx = offScreenLeft != 0 ? offScreenLeft + : Math.min(childLeft - parentLeft, offScreenRight); + } + } else { + dx = 0; + } + + // Favor bringing the top into view over the bottom. If top is already visible and + // we should scroll to make bottom visible, make sure top does not go out of bounds. + final int dy; + if (canScrollVertical) { + dy = offScreenTop != 0 ? offScreenTop : Math.min(childTop - parentTop, offScreenBottom); + } else { + dy = 0; + } + + + if (dx != 0 || dy != 0) { + if (immediate) { + scrollBy(dx, dy); + } else { + smoothScrollBy(dx, dy); + } + // 重绘是为了选中item置顶,具体请参考getChildDrawingOrder方法 + postInvalidate(); + return true; + } + + + return false; + } + + + @Override + public int getBaseline() { + return -1; + } + + + public int getSelectedItemOffsetStart() { + return mSelectedItemOffsetStart; + } + + public int getSelectedItemOffsetEnd() { + return mSelectedItemOffsetEnd; + } + + @Override + public void setLayoutManager(LayoutManager layout) { + super.setLayoutManager(layout); + } + + /** + * 判断是垂直,还是横向. + */ + private boolean isVertical() { + LayoutManager manager = getLayoutManager(); + if (manager != null) { + LinearLayoutManager layout = (LinearLayoutManager) getLayoutManager(); + return layout.getOrientation() == LinearLayoutManager.VERTICAL; + + } + return false; + } + + /** + * 设置选中的Item距离开始或结束的偏移量; + * 与滚动方向有关; + * 与setSelectedItemAtCentered()方法二选一 + * + * @param offsetStart + * @param offsetEnd 从结尾到你移动的位置. + */ + public void setSelectedItemOffset(int offsetStart, int offsetEnd) { + setSelectedItemAtCentered(false); + mSelectedItemOffsetStart = offsetStart; + mSelectedItemOffsetEnd = offsetEnd; + } + + /** + * 设置选中的Item居中; + * 与setSelectedItemOffset()方法二选一 + * + * @param isCentered + */ + public void setSelectedItemAtCentered(boolean isCentered) { + this.mSelectedItemCentered = isCentered; + } + + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + View view = getFocusedChild(); + if (null != view) { + + position = getChildAdapterPosition(view) - getFirstVisiblePosition(); + if (position < 0) { + return i; + } else { + if (i == childCount - 1) {//这是最后一个需要刷新的item + if (position > i) { + position = i; + } + return position; + } + if (i == position) {//这是原本要在最后一个刷新的item + return childCount - 1; + } + } + } + return i; + } + + public int getFirstVisiblePosition() { + if (getChildCount() == 0) + return 0; + else + return getChildAdapterPosition(getChildAt(0)); + } + + public int getLastVisiblePosition() { + final int childCount = getChildCount(); + if (childCount == 0) + return 0; + else + return getChildAdapterPosition(getChildAt(childCount - 1)); + } + + + /*********** + * 按键加载更多 start + **********/ + + private OnLoadMoreListener mOnLoadMoreListener; + + public interface OnLoadMoreListener { + void onLoadMore(); + } + + + public void setOnLoadMoreListener(OnLoadMoreListener onLoadMoreListener) { + this.mOnLoadMoreListener = onLoadMoreListener; + } + + +// +// private PressBackListenner mPressbackLister; +// +// +// public void setOnPressBackListener(PressBackListenner listener) { +// this.mPressbackLister = listener; +// } + + /** + * 设置为0,这样可以防止View获取焦点的时候,ScrollView自动滚动到焦点View的位置 + */ + + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + return 0; + } + + @Override + public void onScrollStateChanged(int state) { + if (state == SCROLL_STATE_IDLE) { + // 加载更多回调 + if (null != mOnLoadMoreListener) { + if (getLastVisiblePosition() >= getAdapter().getItemCount() - (1 + mLoadMoreBeforehandCount)) { + mOnLoadMoreListener.onLoadMore(); + } + } + LayoutManager layoutManager = getLayoutManager(); + GridLayoutManager staggeredGridLayoutManager = (GridLayoutManager) layoutManager; + int spanCount = staggeredGridLayoutManager.getSpanCount(); + int itemCount = staggeredGridLayoutManager.getItemCount(); + Log.i(TAG,"onScrollStateChanged() spanCount="+spanCount+"itemCount="+itemCount); + if(hasResetFocus&&mScrollDirecitonUp&&lastFocusPosition>=spanCount){ + Log.i(TAG,"onScrollStateChanged() hasResetFocus1="+hasResetFocus); + hasResetFocus=false; + setSelectedPosition(lastFocusPosition-spanCount); + View view = layoutManager.findViewByPosition(lastFocusPosition-spanCount); + if (view != null) { + view.requestFocus(); + } + } + if(hasResetFocus&&!mScrollDirecitonUp&&lastFocusPosition - - - - - + @@ -12,4 +12,16 @@ + + + + + + + + + + + +