什么是红点更新提示?

红点更新提示类似微信朋友圈有新的朋友消息 时会在“发现”tab上显示红点,表示有新的消息。

目前三种显示方式:

Android应用中-更新提示显示红点的方案-LMLPHP1.显示具体数字

Android应用中-更新提示显示红点的方案-LMLPHP2.只显示红点

Android应用中-更新提示显示红点的方案-LMLPHP3.显示省略,表示数量很多

方案思路:

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;
}
}
04-28 05:05