酒店信息、客房信息等页面模板设计

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, ", text = " + text);

int index = text.indexOf(ch);

LogUtils.e(TAG, ", index = " + index);

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, ", spannableString = " + spannableString);

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, ", mAdjustTop = " + mAdjustTop + ", mAdjustBottom = " + mAdjustBottom + ", immediate = " + immediate);

        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, ", rect.top = " + rect.top + ", rect.height = " + rect.height());

        final int offScreenTop = Math.min(0, childTop - parentTop);

        final int offScreenBottom = Math.max(0, childBottom - parentBottom);

        if (LogUtils.isOpenLogd()) LogUtils.d(TAG, ", offScreenTop = " + offScreenTop + ", offScreenBottom = " + offScreenBottom);

        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, ", nextchild = " + child + ", focused = " + focused);

        if (!hasFocus()) {

            if (mFocusGainListener != null) {

                mFocusGainListener.onFocusGain(child, focused);

            }

        }

        super.requestChildFocus(child, focused);

        mCurrentFocusPosition = getChildViewHolder(child).getAdapterPosition();

        if (LogUtils.isOpenLogd()) LogUtils.d(TAG, ", focusPos = " + mCurrentFocusPosition);

    }

    @Override

    public void addFocusables(ArrayList views, int direction, int focusableMode) {

        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, ", focusedChild = " + focusedChild);

        if (focusedChild == null) {

            return super.getChildDrawingOrder(childCount, i);

        } else {

            int index = indexOfChild(focusedChild);

            if (LogUtils.isOpenLogd()) LogUtils.d(TAG, ", index = " + index + ", i = " + i + ", count = " + childCount);

            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, ", drawable = " + drawable);

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图片,只有中间拉伸部分是可以显示文字的,这个长度直接决定了能显示多少文字

你可能感兴趣的:(酒店信息、客房信息等页面模板设计)