什么是红点更新提示?
红点更新提示类似微信朋友圈有新的朋友消息 时会在“发现”tab上显示红点,表示有新的消息。
目前三种显示方式:
1.显示具体数字
2.只显示红点
3.显示省略,表示数量很多
方案思路:
1.显示红点:通过本地和服务器的时间戳对比,判断是否要显示红点,每个按钮都有其对应的本地时间戳和服务器时间戳。
2.隐藏红点:当红点显示时,点击把红点隐藏。 并判断是否要更新本地时间戳文件。
3.红点时间戳:时间戳需要进行缓存,方式为写入文件。
重点考虑问题:
1.点击按钮后是否要刷新该项的时间戳
2.由于更新时间戳要写入文件 所以尽快能少文件的保存
答:
1.只在按钮为红点显示状态下点击后更新本地时间戳文件。但在接口请求时不马上写入文件。原因如下:
接口请求过程中 -点击了按钮(无法得知该按钮记录的时间是否在服务器里时间的前面还是后面 保存点击时间 另起临时Temp字段 不覆盖当前本地Local时间字段)
(等请求完后调用回调,再对临时字段的时间进行判断比较)
接口请求成功 -点击了按钮(如果!showing的话,直接返回。如果showing,改变状态,更新时间戳)
2.后台接口参数加入index字段,每次后台有更新服务器时间戳时将index升高,前端请求后对index进行判断,不同则刷新本地时间戳并保存到文件,index相等说明数据库时间戳没有更新,就不处理。
方案说明安排:
方案从后台的字段设计和前端的逻辑处理进行说明,代码所列的顺序为:
后台接口参数--》红点控件(继承ImageView)--》客服端接口类及方法(单例)--》监听类和方法(方法写在接口类中)--》Activity类中的方法
--》工具类Utils的方法(供Activity调用)--》时间戳Model(UserItemUpdateRecord)
接口参数说明
接口:http://X.XX.XX.XXX/api/get_user_item_update_status?
请求方式GET
参数说明:
token=DQR3WF6Q56QW56QW //用户唯一识别码 可在后台做分组 拓展参数
output { "code":0,//状态码,0代表成功 "msg":"",//发生错误的原因 "data":{//具体数据
"index":0,//当前点击记录 "tip":[//各点时间戳 { "type":"home", "date":"2015-09-01",
"count":0,//内容更新数量
}, { "type":"infomation", "date":"2015-11-11",
"count":5,
}, { "type":"me", "date":"2016-01-15",
"count":15,
}
]
}
RedPointImageView
这里红点控件根据数量分成三类,数量判断标准自己定。
public class RedPointImageView extends ImageView { //红点长度类型 private static final int TYPE_ZERO = 0; private static final int TYPE_SHORT = 1; private static final int TYPE_LONG = 2; private int type; //保存onSizeChange()里的宽高 private int width; private int height; //按钮Tag,用来识别 private String mTag; private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Rect mRect = new Rect(); private RelativeLayout mRlPoint = null; private Drawable mDPoint = null; private int radius; private boolean mShow = false; private int number; private TextView mTvPoint; public RedPointImageView(Context context) { super(context); this.number = -1; init(); } public RedPointImageView(Context context, AttributeSet attrs) { super(context, attrs); this.number = -1; init(); } public RedPointImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.number = -1; init(); } private void init() { if(number<0)return; //数量小于0直接返回 mPaint.setFilterBitmap(true); radius = getResources().getDimensionPixelSize(R.dimen.red_point_radius); mRlPoint = new RelativeLayout(getContext()); mTvPoint = new TextView(getContext()); mTvPoint.setTextSize(14); mTvPoint.setTextColor(getResources().getColor(R.color.white)); RelativeLayout.LayoutParams params1 = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); params1.setMargins(0,0,0,0); params1.addRule(RelativeLayout.CENTER_IN_PARENT); mRlPoint.addView(mTvPoint,params1); initUI(); } private void initUI(){ if(number == 0){ //ZERO类型 mTvPoint.setText(""); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(getResources().getDimensionPixelOffset(R.dimen.margin_8),getResources().getDimensionPixelOffset(R.dimen.margin_8)); params.setMargins(0,0,0,0); mRlPoint.setLayoutParams(params); mRlPoint.setBackgroundResource(R.drawable.icon_red_point); type = TYPE_ZERO; }else if(number>0&&number<10){ //SHORT类型 mTvPoint.setText(String.valueOf(number)); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(getResources().getDimensionPixelOffset(R.dimen.margin_15),getResources().getDimensionPixelOffset(R.dimen.margin_15)); params.setMargins(0,0,0,0); mRlPoint.setLayoutParams(params); mRlPoint.setBackgroundResource(R.drawable.icon_red_point); type = TYPE_SHORT; }else{ //LONG类型 mTvPoint.setText("···"); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(getResources().getDimensionPixelOffset(R.dimen.margin_20),getResources().getDimensionPixelOffset(R.dimen.margin_12)); params.setMargins(0,0,0,0); mRlPoint.setLayoutParams(params); mRlPoint.setBackgroundResource(R.drawable.bg_corner_red); type = TYPE_LONG; } mDPoint = new BitmapDrawable(null,convertViewToBitmap(mRlPoint)); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = w; height = h; updateRect(w, h); } private void updateRect(int w, int h) { int left,top,right,bottom; if(type == TYPE_SHORT){ left = w - radius; top = 0; right = w; bottom = radius; }else if(type == TYPE_ZERO){ left = w - radius*2/3; top = 0; right = w; bottom = radius*2/3; }else{ left = w - radius/3*4; top = 0; right = w; bottom = radius/5*4; } mRect.set(left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mShow) { drawRedPoint(canvas); } } private void drawRedPoint(Canvas canvas) { if (mDPoint == null) { return; } canvas.save(); // canvas.clipRect(mRect, Region.Op.DIFFERENCE); mDPoint.setBounds(mRect); mDPoint.draw(canvas); canvas.restore(); } public void setShow(boolean isShow){ this.mShow = isShow; invalidate(); } public boolean isShow(){ return mShow; } public String getmTag() { return mTag; } public void setmTag(String mTag) { this.mTag = mTag; } public void updateItem(){ UserItemUpdateRecord userItemUpdateRecord = IpinClient.getInstance().getAccountManager().getUserItemUpdateRecord(); if(userItemUpdateRecord!=null){ userItemUpdateRecord.refreshUpdateImg(this); } } public void setNumber(int number){ this.number = number; if(number<0) mShow = false; else mShow = true; init(); onSizeChanged(width,height,width,height); invalidate(); } public static Bitmap convertViewToBitmap(View view){ view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); view.buildDrawingCache(); Bitmap bitmap = view.getDrawingCache(); return bitmap; } }
接口类方法
1.tryToLoadUserItemStatus() --> 从本地文件里读取红点时间戳对象
2.requestUserItemStatus() --> 接口请求服务器端的红点时间戳
3.saveItemUpdateStatusToFile() --> 保存红点时间戳到本地文件
public static final String URL_GET_USER_ITEM_STATUS = "http://m.gaokao.ipin.com/api/get_user_item_update_status?";//获取用户按钮更新信息 public static final String FILE_NAME_USER_ITEM_STATUS = "user_item_status.ipin";//按钮更新状态文件 private AtomicBoolean isLoadedUserItemStatus = new AtomicBoolean(false); private HashSet<OnUpdateItemStatusListener> mOnUpdateItemStatusListeners = new HashSet<>();//监听列表 private UserItemUpdateRecord userItemUpdateRecord;//时间戳model private boolean isRequestingUserItemStatus = false;//是否正在获取数据 /** * 从文件中读取按钮更新信息 */ private void tryToLoadUserItemStatus(){ TaskExecutor.getInstance().post(new Runnable() { @Override public void run() { synchronized (AccountManager.class) { boolean b = checkAndCopyUserData(AccountConstant.FILE_NAME_USER_ITEM_STATUS); if (!b) { isLoadedUserItemStatus.set(true); requestUserItemStatus(); return; } String path = StorageManager.getInstance().getPackageFiles() + AccountConstant.FILE_NAME_USER_ITEM_STATUS; Object object = FileUtil.readObjectFromPath(path); if (object != null && object instanceof UserItemUpdateRecord) { userItemUpdateRecord = (UserItemUpdateRecord) object; } isLoadedUserItemStatus.set(true); requestUserItemStatus(); } } }); }
private boolean checkAndCopyUserData(String path) {
String newPath = StorageManager.getInstance().getPackageFiles() + path;
String oldPath = StorageManager.getInstance().getPackageCacheRoot() + path;
File newFile = new File(newPath);
if (newFile.exists()) {
return true;
}
File oldFile = new File(oldPath);
if (!oldFile.exists()) {
return false;
}
return oldFile.renameTo(newFile);
}
/** * 请求服务器更新按钮的时间戳 */ public void requestUserItemStatus(){ isRequestingUserItemStatus = true; final IRequest request = (IRequest) IpinClient.getInstance().getService(IpinClient.SERVICE_HTTP_REQUEST); if (request == null) { return; } request.sendRequestForPostWithJson(AccountConstant.URL_GET_USER_ITEM_STATUS, getParamForGetUserInfo(getIpinToken()), new IRequestCallback() { @Override public void onResponseSuccess(JSONObject jsonObject) { if (jsonObject == null) { return; } if(jsonObject.getInteger(Constant.KEY_CODE)!=0)return; if(jsonObject.getJSONObject(Constant.KEY_DATA)==null)return; jsonObject = jsonObject.getJSONObject(Constant.KEY_DATA); if(userItemUpdateRecord!=null&&userItemUpdateRecord.getIndex() == jsonObject.getInteger("index"))
return;//如果服务器的index与本地的一样,则目前已经保存最新的更新时间戳,不执行更新本地时间戳操作 UserItemUpdateRecord updateRecord = new UserItemUpdateRecord(); updateRecord.decode(jsonObject); userItemUpdateRecord = updateRecord; isRequestingUserItemStatus = false; dispatchOnUpdateItemStatusListener(); TaskExecutor.getInstance().post(new Runnable() { @Override public void run() { saveItemUpdateStatusToFile();//将时间戳保存至文件 } }); } @Override public void onResponseSuccess(String str) { isRequestingUserItemStatus = false; } @Override public void onResponseError(int code) { isRequestingUserItemStatus = false; } }); } /** * 保存用户按钮更新信息 */ private void saveItemUpdateStatusToFile() { TaskExecutor.getInstance().post(new Runnable() { @Override public void run() { if (userItemUpdateRecord != null) { String path = StorageManager.getInstance().getPackageFiles() + AccountConstant.FILE_NAME_USER_ITEM_STATUS;//path为文件路径 FileUtil.writeObjectToPath(userItemUpdateRecord, path); } } }); }
接口请求完的服务器时间戳需要保存到文件里,并更新本地model
监听类及方法
public interface OnUpdateItemStatusListener { void updateItemStatus(); } private HashSet<OnUpdateItemStatusListener> mOnUpdateItemStatusListeners = new HashSet<>(); public void registerOnUpdateItemStatusListener(OnUpdateItemStatusListener listener) { mOnUpdateItemStatusListeners.add(listener); } public void unregisterOnUpdateItemStatusListener(OnUpdateItemStatusListener listener) { mOnUpdateItemStatusListeners.remove(listener); } private void dispatchOnUpdateItemStatusListener() { for (OnUpdateItemStatusListener listener : mOnUpdateItemStatusListeners) { listener.updateItemStatus(); } }
使用红点的类中的方法
private RedPointImageView mButton1;
private RedPointImageView mButton2;
private RedPointImageView mButton3;
mButton1.setmTag(UserItemUpdateRecord.KEY_HOME);//给按钮设置备注,可做判断识别
mButton2.setmTag(UserItemUpdateRecord.KEY_INFORMATION);
mButton3.setmTag(UserItemUpdateRecord.KEY_ME);
MyClient.getInstance().getAccountManager().registerOnUpdateItemStatusListener(this);//注册监听 @Override public void updateItemStatus() {//监听回调方法 checkAndUpdateItemStatus(); } private void checkAndUpdateItemStatus(){ List<RedPointImageView> views = new ArrayList<>(); views.add(mButton1); views.add(mButton2); views.add(mButton3); MyUtils.updateRedPointItem(getActivity(),views);//调用工具类中的方法 } @Override public void onDestroy() { super.onDestroy(); IpinClient.getInstance().getAccountManager().unregisterOnUpdateItemStatusListener(this);//注销监听 } //点击时按钮调用方法 mButton.updateItem();
工具类MyUtils中的方法
/** * 检查更新按钮红点状态 * @param imageView */ public static void updateRedPointItem(List<RedPointImageView> imageView){ for (RedPointImageView view : imageView){ updateRedPointItem(view); } } public static void updateRedPointItem(RedPointImageView imageView){ UserItemUpdateRecord userItemUpdateRecord = IpinClient.getInstance().getAccountManager().getUserItemUpdateRecord(); if(userItemUpdateRecord.isNeedUpdate(imageView)){ imageView.setShow(true); } }
UserItemUpdateRecord类
public class UserItemUpdateRecord implements Serializable, IParse { private static final String KEY_INDEX = "index"; //这里拿了三个按钮作为例子,每一个按钮需要配置一个字符串(Json转换)和 //三个参数(服务器时间戳mDSystemXX、移动端时间戳mDXX、移动端临时时间戳mDTempXX,这个临时时间戳的作用前面已经说明) public static final String KEY_HOME = "home"; public static final String KEY_INFORMATION = "information"; public static final String KEY_ME = "me"; private int index; private Date mDHome; private Date mDTempHome; private Date mDSystemHome; private Date mDInformation; private Date mDTempInformation; private Date mDSystemInformation; private Date mDMe; private Date mDTempMe; private Date mDSystemMe; public UserItemUpdateRecord() { mDTempHome = getDateForTemp("2015-01-01 01:01:01"); mDTempInformation = getDateForTemp("2015-01-01 01:01:01"); mDTempMe = getDateForTemp("2015-01-01 01:01:01"); mDHome = new Date(); mDTempHome = new Date(); mDSystemHome = new Date(); } public void decode(JSONObject object){ if(index == object.getInteger(KEY_INDEX))return; index = object.getInteger(KEY_INDEX); mDSystemHome = object.getDate(KEY_HOME); mDSystemInformation = object.getDate(KEY_INFORMATION); mDSystemMe = object.getDate(KEY_ME); if(mDHome==null)mDHome = mDSystemHome; if(mDInformation==null)mDInformation = mDSystemInformation; if(mDMe==null)mDMe = mDSystemMe; } @Override public JSONObject encode(Object o) { return null; } @Override public void release() { } /** * 判断是否需要显示红点 * @param imageView * @return */ public boolean isNeedUpdate(RedPointImageView imageView){ String tag = imageView.getmTag(); switch (tag){ case KEY_HOME: return judgeIsNeedUpdate(imageView,mDHome,mDTempHome,mDSystemHome); case KEY_INFORMATION: return judgeIsNeedUpdate(imageView,mDInformation,mDTempInformation,mDSystemInformation); case KEY_ME: return judgeIsNeedUpdate(imageView,mDMe,mDTempMe,mDSystemMe); default: return false; } } /** * 只有当mDSystem在mDLocal、mDTemp之后才需要显示 * @param mDLocal 本地点击时间 * @param mDTemp 点击最新时间 * @param mDSystem 系统更新时间 * @return */ private boolean judgeIsNeedUpdate(RedPointImageView view,Date mDLocal ,Date mDTemp,Date mDSystem){ if(mDLocal.before(mDSystem)){ if(mDTemp==null)mDTemp = new Date(mDLocal.getTime()); if(mDSystem.before(mDTemp)){ //判断方法加入刷新动作 这里处理了前面说到的接口请求过程中点击按钮,把时间保存在临时Temp参数,这里进行判断并处理,可减少写入文件次数。 mDLocal.setTime(mDTemp.getTime()); executeUpdate(view,mDInformation,mDTempInformation); //刷新 return false; }else{ return true; } }else{ return false; } } /** * 点击时触发的处理方法 * @param view */ public void refreshUpdateImg(RedPointImageView view){ String tag = view.getmTag(); switch (tag){ case KEY_HOME: executeUpdate(view,mDHome,mDTempHome); break; case KEY_INFORMATION: executeUpdate(view,mDInformation,mDTempInformation); break; case KEY_ME: executeUpdate(view,mDMe,mDTempMe); break; } } private void executeUpdate(RedPointImageView view,Date mDLocal ,Date mDTemp){ boolean flag = IpinClient.getInstance().getAccountManager().isRequestingUserItemStatus(); if(flag){ mDTemp.setTime(new Date().getTime());//只更新Temp时间,等待接口请求完刷新状态的时候做是否要保存点击时间的判断 if(view.isShow()){ view.setShow(false); } }else{ if(view.isShow()){ //接口已经请求完 并且处于红点显示状态,使红点不显示,并且保存当前点击时间 mDLocal.setTime(new Date().getTime()); IpinClient.getInstance().getAccountManager().saveItemUpdateStatusToFile(); view.setShow(false); }else{ //接口已经请求完 并且处于红点不显示状态,不做时间保存处理 } } } private Date getDateForTemp(String time){ Date date = new Date(); SimpleDateFormat df=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); try { date = df.parse(time); } catch (ParseException e) { e.printStackTrace(); } return date; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public Date getmDHome() { return mDHome; } public void setmDHome(Date mDHome) { this.mDHome = mDHome; } public Date getmDInformation() { return mDInformation; } public void setmDInformation(Date mDInformation) { this.mDInformation = mDInformation; } public Date getmDMe() { return mDMe; } public void setmDMe(Date mDMe) { this.mDMe = mDMe; } public Date getmDSystemMe() { return mDSystemMe; } public void setmDSystemMe(Date mDSystemMe) { this.mDSystemMe = mDSystemMe; } public Date getmDSystemHome() { return mDSystemHome; } public void setmDSystemHome(Date mDSystemHome) { this.mDSystemHome = mDSystemHome; } public Date getmDSystemInformation() { return mDSystemInformation; } public void setmDSystemInformation(Date mDSystemInformation) { this.mDSystemInformation = mDSystemInformation; } }