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

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

int index = text.indexOf(ch);

LogUtils.e(TAG, "<setSpannableTextView>, 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, "<setSpannableTextView>, 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, "<requestChildRectangleOnScreen>, 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, "<requestChildRectangleOnScreen>, 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, "<requestChildRectangleOnScreen>, 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, "<requestChildFocus>, 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, "<requestChildFocus>, focusPos = " + mCurrentFocusPosition);

    }

    @Override

    public void addFocusables(ArrayList<View> 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, "<getChildDrawingOrder>, focusedChild = " + focusedChild);

        if (focusedChild == null) {

            return super.getChildDrawingOrder(childCount, i);

        } else {

            int index = indexOfChild(focusedChild);

            if (LogUtils.isOpenLogd()) LogUtils.d(TAG, "<getChildDrawingOrder>, 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, "<setSelectItemIconVisible>, 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图片,只有中间拉伸部分是可以显示文字的,这个长度直接决定了能显示多少文字

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351