1、相关的View、Activity
Activity: HotelInfoActivity
页面标题: TextView mTitleTextView
页面标题左侧icon: ImageView mTitleImageView
信息列表: RecyclerView, 定焦需求 mInfoListView
信息资源(图片轮播、视频): MediaInfoView(自定义view) mMediaInfoView
信息介绍: TextView(多行换行显示) mInfoIntrTextView
资源序号: MediaIndexView(自定义view) mInfoIndexTextView
按键说明: TextView mOperationTextView
2、业务接口
信息页数据请求: NetReqMgr mNetReqMgr
列表项和信息资源的对应: HotelInfoManager mHotelInfoMgr
数据请求回调: HotelInfoDataListener mHotelInfoDataListener
信息资源播放: HotelMediaInfoPlayer mHotelInfoPlayer
图片轮播: HotelPicturePlayer mHotelPicPlayer
视频播放: HotelVideoPlayer mHotelVideoPlayer
3、数据及业务处理
网络接口及数据处理 HotelInfoModule mHotelInfoModule
4、框架设计
V---相关的View、Activity
P---业务逻辑设计的接口
M---数据及业务处理
5、业务流程:
a) 初始化背景图、固定UI;
b) 初始化业务接口及数据回调;
c) 发起网络请求NetReqMgr,获取到数据存储到HotelInfoModule中管理;
d) 初始化标题、信息列表、信息资源对应的资源
修改记录:
1、添加SliderView的library库,放在根目录下,launcher_v4\launcher_v4\build.gradle的dependencies添加如下:
compile project(':library')
2、添加hotelinfo包,以及目录下文件
M---
HotelInfoModule.java
HotelInfoHttpFactory.java
HotelInfoBean.java
Data.java
List_block.java
List_element.java
V---
HotelInfoActivity.java
HotelRecyclerViewAdapter.java
HotelInfoIndexView.java
HotelInfoItemView.java
HotelInfoVideoView.java
P---
HotelInfoDataListener.java
HotelInfoManager.java
HotelMediaInfoPlayer.java
HotelNetRequestMgr.java
HotelPicturePlayer.java
HotelVideoPlayer.java
技术点:
1、TextView中插入小图片
利用SpannableString的特性,添加小图片:
private void setSpannableTextView(TextView textView, int strId, int iconId) {
final String ch = "_";
SpannableString spannableString;
ImageSpan imageSpan;
Drawable drawable;
String text = mContext.getString(strId);
LogUtils.e(TAG, "
int index = text.indexOf(ch);
LogUtils.e(TAG, "
if (index >= 0) {
spannableString = new SpannableString(text);
imageSpan = new ImageSpan(mContext, iconId, ImageSpan.ALIGN_BASELINE);
spannableString.setSpan(imageSpan, index, index + ch.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
LogUtils.e(TAG, "
textView.setText(spannableString);
}
}
2、RecyclerView增加item背景
在Adapter中的onBindViewHolder中添加以下逻辑:
@Override
public void onBindViewHolder(InfoListViewHolder holder, int position) {
holder.itemView.setFocusable(true);
holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
if (hasFocus) {
// 落焦背景图和文字颜色
holder.itemView.setBackgroundResource(R.drawable.hotel_list_item_focus_bg);
holder.mTextView.setTextColor(mContext.getResources().getColor(R.color.black));
} else {
// 非落焦背景图和文字颜色
holder.itemView.setBackgroundResource(R.drawable.hotel_list_item_unfocus_bg);
holder.mTextView.setTextColor(mContext.getResources().getColor(R.color.white));
}
}
});
holder.mTextView.setText(mInfoList.get(position));
}
3、RecyclerView去掉item之间的分隔线
重写Decoration,即以下添加的item decoration,默认是DividerItemDecoration:
mRecyclerView.addItemDecoration(new MyItemDecoration(mContext, DividerItemDecoration.VERTICAL));
主要是重写以下函数getItemOffsets,将下面的bottom改为0
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, 0/*mDivider.getIntrinsicHeight()*/);
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
详细源码如下:
public class MyItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private static final String TAG = "DividerItem";
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
private Drawable mDivider;
/**
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
private int mOrientation;
private final Rect mBounds = new Rect();
/**
* Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
* {@link LinearLayoutManager}.
*
* @param context Current context, it will be used to access resources.
* @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
public MyItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
if (mDivider == null) {
Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
setOrientation(orientation);
}
/**
* Sets the orientation for this divider. This should be called if
* {@link RecyclerView.LayoutManager} changes orientation.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}
/**
* Sets the {@link Drawable} for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = 0;//mBounds.bottom + Math.round(child.getTranslationY());
final int top = 0;//bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
final int top;
final int bottom;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top,
parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(child.getTranslationX());
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, 0/*mDivider.getIntrinsicHeight()*/);
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
4、RecyclerView定焦需求,上下三分之一位置固定焦点
需要自定义LinearLayoutManager,即RecyclerView设置的
mRecyclerView.setLayoutManager(layoutManager);
详细源码见MyLinearLayoutMananger.java, 重写requestChildRectangleOnScreen, mAdjustTop和mAdjustBottom用于控制离顶部和底部多少距离开始滚动列表
public class MyLinearLayoutMananger extends LinearLayoutManager {
private static final String TAG = "HotelInfo_" + "LayoutMgr";
private static final int SINGLE_ITEM_HEIGHT = DisplayUtil.heightOf(72);
private final int mAdjustTop = SINGLE_ITEM_HEIGHT * 2;
private final int mAdjustBottom = SINGLE_ITEM_HEIGHT * 2;
public MyLinearLayoutMananger(Context context) {
super(context);
}
public MyLinearLayoutMananger(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
@Override
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, @NonNull View child, @NonNull Rect rect, boolean immediate, boolean focusedChildVisible) {
if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "
final int parentTop = getPaddingTop() + mAdjustTop;
final int parentBottom = getHeight() - getPaddingBottom() - mAdjustBottom;
final int childTop = child.getTop() + rect.top;
final int childBottom = childTop + rect.height();
if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "
final int offScreenTop = Math.min(0, childTop - parentTop);
final int offScreenBottom = Math.max(0, childBottom - parentBottom);
if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "
int dy = offScreenTop != 0 ? offScreenTop : offScreenBottom;
if (dy != 0) {
if (immediate) {
parent.scrollBy(0, dy);
} else {
parent.smoothScrollBy(0, dy);
}
return true;
}
return false;
}
}
5、RecyclerView焦点记忆
重写focusSearch,源码参考MyRecyclerView.java
public class MyRecyclerView extends RecyclerView {
private static final String TAG = "HotelInfo_" + "RecyclerView";
private boolean mCanFocusOutHorizontal = true;
private FocusLostListener mFocusLostListener;
private FocusGainListener mFocusGainListener;
private int mCurrentFocusPosition = 0;
public MyRecyclerView(Context context) {
this(context, null);
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
setChildrenDrawingOrderEnabled(true);
setItemAnimator(null);
this.setFocusable(true);
}
public boolean isCanFocusOutHorizontal() {
return mCanFocusOutHorizontal;
}
public void setCanFocusOutHorizontal(boolean canFocusOutHorizontal) {
mCanFocusOutHorizontal = canFocusOutHorizontal;
}
@Override
public View focusSearch(int direction) {
return super.focusSearch(direction);
}
@Override
public View focusSearch(View focused, int direction) {
LogUtils.i(TAG, "focusSearch " + focused + ",direction= " + direction);
View view = super.focusSearch(focused, direction);
if (focused == null) {
return view;
}
if (view != null) {
View nextFocusItemView = findContainingItemView(view);
if (nextFocusItemView == null) {
if (!mCanFocusOutHorizontal && (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
return focused;
}
if (mFocusLostListener != null) {
mFocusLostListener.onFocusLost(focused, direction);
}
return view;
}
}
return view;
}
public void setFocusLostListener(FocusLostListener focusLostListener) {
this.mFocusLostListener = focusLostListener;
}
public interface FocusLostListener {
void onFocusLost(View lastFocusChild, int direction);
}
public void setGainFocusListener(FocusGainListener focusListener) {
this.mFocusGainListener = focusListener;
}
public interface FocusGainListener {
void onFocusGain(View child, View focued);
}
@Override
public void requestChildFocus(View child, View focused) {
if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "
if (!hasFocus()) {
if (mFocusGainListener != null) {
mFocusGainListener.onFocusGain(child, focused);
}
}
super.requestChildFocus(child, focused);
mCurrentFocusPosition = getChildViewHolder(child).getAdapterPosition();
if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "
}
@Override
public void addFocusables(ArrayList
View view = null;
if (getLayoutManager() != null) {
view = getLayoutManager().findViewByPosition(mCurrentFocusPosition);
}
if (this.hasFocus() || mCurrentFocusPosition < 0 || view == null) {
super.addFocusables(views, direction, focusableMode);
} else if (view.isFocusable()) {
views.add(view);
} else {
super.addFocusables(views, direction, focusableMode);
}
}
/**
*
* @param childCount
* @param i
* @return
*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
View focusedChild = getFocusedChild();
if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "
if (focusedChild == null) {
return super.getChildDrawingOrder(childCount, i);
} else {
int index = indexOfChild(focusedChild);
if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "
if (i == childCount - 1) {
return index;
}
if (i < index) {
return i;
}
return i + 1;
}
}
}
6、RecyclerView焦点丢失问题
1.adapter的setHasStableIds设置成true
2.重写adapter的getItemId方法
@Override
public long getItemId(int position) {
return position;
}
3.mRecyclerView.setItemAnimator(null);
拓展:
RecyclerView,LayoutManager,Adapter,ViewHolder,ItemDecoration之间的关系
7、RecyclerView item文字超长滚动显示
在控件布局中配置
android:focusable="true"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
在adapter的onFocusChange中添加:
mTextView.setSelected(true);
8、RecyclerView item动画超出边界
在父布局的父布局中添加android:clipChildren="false",允许view超出布局边界
9、RecyclerView的边界动效
Launcher的AnimUtils已经封装为工具类,只需传入需要做动画的view,例如
焦点动效:AnimUtils.doFocusedScaleAnim(itemView);
失焦点动效:AnimUtils.doUnFocusScaleAnim(itemView);
向上移动动效:AnimUtils.doDirectionAnim(View.FOCUS_UP, focusView);
向下移动动效:AnimUtils.doDirectionAnim(View.FOCUS_DOWN, focusView);
10、UE一般要求RecyclerView的item之间留有阴影,要求两个item有重叠部分,所以在设置item的布局时,注意给marginTop设置负值
mHotelInfoUtils.setFrameLayoutParams(itemView,
HotelInfoUtils.HOTEL_INFO_LIST_ITEM_WIDTH,
HotelInfoUtils.HOTEL_INFO_LIST_ITEM_HEIGHT,
0,
-1 * HotelInfoUtils.HOTEL_INFO_RECYCLER_MARGIN_TOP);
public FrameLayout.LayoutParams setFrameLayoutParams(View view, int width, int height, int marginStart, int marginTop, int marginRight, int marginBottom) {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height);
params.setMargins(marginStart, marginTop, marginRight, marginBottom);
view.setLayoutParams(params);
return params;
}
11、RecyclerView item显示选中icon
TextView调用setCompoundDrawables可以设置,利用drawable的setBounds设置icon宽高位置,利用setCompoundDrawablePadding设置文字和icon的间距:
Drawable drawable = mContext.getResources().getDrawable(R.drawable.hotel_item_sel);
LogUtils.e(TAG, "
if (drawable != null) {
drawable.setBounds(0, 0, DisplayUtil.widthOf(4), DisplayUtil.heightOf(28));
textView.setCompoundDrawablePadding(DisplayUtil.heightOf(18));
textView.setCompoundDrawables(drawable, null, null, null);
}
碰到一个问题,icon显示不出来,网上说要先调用setBounds,这里和TextView的宽度设置有关,但是由于设置TextView宽度用于末尾显示省略号,最终调用
TextView.setMaxWidth设置文字宽度,而不是设置TextView的布局宽度解决:
if (mTextView != null) {
mTextView.setTextColor(mContext.getResources().getColor(R.color.white));
mTextView.setSingleLine();
mTextView.setFocusable(true);
// 7 words and 1 ellipsis, about 30ps one word
mTextView.setMaxWidth(HotelInfoUtils.HOTEL_INFO_LIST_TEXT_WIDTH);
mTextView.setEllipsize(TextUtils.TruncateAt.END);
mTextView.setMarqueeRepeatLimit(0);
mTextView.setSelected(false);
}
另外,item高亮图片对显示个数有影响,因为用的xx.9.png图片,只有中间拉伸部分是可以显示文字的,这个长度直接决定了能显示多少文字