├── .gitignore ├── README.md ├── ant.properties ├── app ├── AndroidManifest.xml ├── build.gradle ├── res │ ├── color │ │ └── selector_comment_name.xml │ ├── drawable-hdpi │ │ └── ic_launcher.png │ ├── drawable-ldpi │ │ └── ic_launcher.png │ ├── drawable-mdpi │ │ └── ic_launcher.png │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ ├── drawable │ │ └── bg_input_comment.xml │ ├── layout │ │ ├── item_header.xml │ │ ├── item_moment.xml │ │ ├── main.xml │ │ ├── view_comment_list_item.xml │ │ └── view_input_comment.xml │ └── values │ │ ├── strings.xml │ │ └── styles.xml └── src │ └── com │ └── example │ └── QzoneComment │ ├── CommentFun.java │ ├── CustomTagHandler.java │ ├── MainActivity.java │ ├── MomentAdapter.java │ └── model │ ├── Comment.java │ ├── Moment.java │ └── User.java ├── build.gradle ├── build.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── proguard-project.txt ├── project.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle 3 | build 4 | local.properties 5 | reports 6 | 7 | # Maven 8 | target 9 | pom.xml.* 10 | release.properties 11 | gen-external-apklibs 12 | 13 | # Eclipse 14 | .classpath 15 | .project 16 | .settings 17 | eclipsebin 18 | 19 | # IntelliJ IDEA 20 | .idea 21 | *.iml 22 | *.ipl 23 | *.iws 24 | classes/ 25 | idea-classes/ 26 | coverage-error.log 27 | 28 | # Android 29 | gen 30 | bin 31 | project.properties 32 | out 33 | 34 | # Finder 35 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [《Android仿手机QQ空间动态评论,自动定位到输入框》](http://blog.csdn.net/u012964944/article/details/51153686) 2 | 3 | 手机QQ空间浏览好友动态时,可以直接对动态评论,点击某条评论,动态列表自动滚动,使输入框刚好在该评论下面,而不会覆盖住评论内容。如下图所示, 4 | 5 | ![img](https://raw.githubusercontent.com/1993hzw/common/master/QZoneComment/01.png) 6 | 7 | 首先要实现输入框刚好在输入面板上面,且动态列表不会被挤上去。可以使用对话框的形式,这样输入框不会影响原有的布局,弹出的对话框布局如下所示,点击EditText时,红色块的内容将位于输入法上面。在这里我把ScrollerView的背景设为透明。其实QQ空间的输入框也是以对话框的形式弹出,因为弹出对话框时,原本全屏的布局突然多了一条状态栏。 8 | 9 | 接着就是要让点击的评论刚好在评论输入框上面,可以使用ListView.smoothScrollBy(distance, duration)让列表滚动到相应位置,但是难点是如何计算出滚动的距离distance。 10 | 如下图所示,我们主要计算弹出输入面板后的输入框和评论之间的距离(绿色线条的长度)。通过View.getLocationOnScreen()方法可以计算出view在屏幕上的坐标(x,y),那么列表滑动的距离distance = listview的y坐标 - 输入框的y坐标。 11 | 12 | ![img](https://raw.githubusercontent.com/1993hzw/common/master/QZoneComment/02.png) 13 | 14 | [CommonFun](https://github.com/1993hzw/QZoneComment/blob/master/app/src/com/example/QzoneComment/CommentFun.java) 15 | 16 | 最后要弄的就是评论中,评论者、接收者和评论内容用不同的颜色显示,且点击时有点击效果。这里可以通过下面的代码实现, 17 | ```java 18 | TextView.setText(Html.fromHtml(content, imageGettor, tagHandler)) 19 | ``` 20 | 自定义标签,通过自定义的标签解析类Html.TagHandler来响应不同标签的操作。这里我自定义了commentator、receiver、content标签,列入一条评论的字符串形式为“用户1 回复 用户2:评论内容”,点击content标签时对该评论的评论者进行回复。 21 | [CustomTagHandler](https://github.com/1993hzw/QZoneComment/blob/master/app/src/com/example/QzoneComment/CustomTagHandler.java) 22 | 23 | 在ListView中,因为Item里面的子View使用了ClickableSpan,导致ListView的OnItemClickListener失效,解决的方法可以在getView中加入下列代码,阻止ListView里面的子View拦截焦点。 24 | ```java 25 | public View getView(int position, View convertView, ViewGroup parent) { 26 | if (convertView != null) { 27 | //防止ListView的OnItemClick与item里面子view的点击发生冲突 28 | ((ViewGroup) convertView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 29 | } 30 | } 31 | ``` 32 | 33 | 设置点击的文字背景色: 34 | 35 | ```xml 36 | 41 | 43 | ``` 44 | 45 | 难点都已经解决了,最后实现的效果如下: 46 | 47 | ![img](https://raw.githubusercontent.com/1993hzw/common/master/QZoneComment/03.gif) 48 | 49 | -------------------------------------------------------------------------------- /ant.properties: -------------------------------------------------------------------------------- 1 | # This file is used to override default values used by the Ant build system. 2 | # 3 | # This file must be checked into Version Control Systems, as it is 4 | # integral to the build system of your project. 5 | 6 | # This file is only used by the Ant script. 7 | 8 | # You can use this to override default values such as 9 | # 'source.dir' for the location of your java source folder and 10 | # 'out.dir' for the location of your output folder. 11 | 12 | # You can also use it define how the release builds are signed by declaring 13 | # the following properties: 14 | # 'key.store' for the location of your keystore and 15 | # 'key.alias' for the name of the key to use. 16 | # The password will be asked during the build when you use the 'release' target. 17 | 18 | -------------------------------------------------------------------------------- /app/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.example.qzonecomment" // 应用包名 9 | minSdkVersion 9 10 | targetSdkVersion 19 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | lintOptions { 21 | abortOnError false 22 | } 23 | 24 | sourceSets { 25 | main { 26 | manifest.srcFile 'AndroidManifest.xml' 27 | java.srcDirs = ['src'] 28 | resources.srcDirs = ['src'] 29 | aidl.srcDirs = ['src'] 30 | renderscript.srcDirs = ['src'] 31 | res.srcDirs = ['res'] 32 | assets.srcDirs = ['assets'] 33 | jni.srcDirs = ['jni'] 34 | jniLibs.srcDirs = ['libs'] 35 | } 36 | } 37 | 38 | } 39 | 40 | dependencies { 41 | compile fileTree(include: ['*.jar'], dir: 'libs') 42 | // compile project(':AndroidsLib') 43 | } -------------------------------------------------------------------------------- /app/res/color/selector_comment_name.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1993hzw/QZoneComment/911ccbf6760011f5da6772a5ac917a87a07dde9b/app/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1993hzw/QZoneComment/911ccbf6760011f5da6772a5ac917a87a07dde9b/app/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1993hzw/QZoneComment/911ccbf6760011f5da6772a5ac917a87a07dde9b/app/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1993hzw/QZoneComment/911ccbf6760011f5da6772a5ac917a87a07dde9b/app/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/res/drawable/bg_input_comment.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/res/layout/item_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | -------------------------------------------------------------------------------- /app/res/layout/item_moment.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 14 | 22 | 31 | 40 | 41 | 47 | 50 | 56 | 57 | 73 | -------------------------------------------------------------------------------- /app/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/res/layout/view_comment_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /app/res/layout/view_input_comment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 18 | 30 | 44 | 54 | 55 | -------------------------------------------------------------------------------- /app/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | QzoneComment 4 | 5 | -------------------------------------------------------------------------------- /app/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/com/example/QzoneComment/CommentFun.java: -------------------------------------------------------------------------------- 1 | package com.example.QzoneComment; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.os.Handler; 7 | import android.text.Html; 8 | import android.text.method.LinkMovementMethod; 9 | import android.view.View; 10 | import android.widget.*; 11 | import com.example.QzoneComment.model.Comment; 12 | import com.example.QzoneComment.model.User; 13 | 14 | import java.util.ArrayList; 15 | 16 | /** 17 | * 评论相关方法 18 | */ 19 | public class CommentFun { 20 | public static final int KEY_COMMENT_SOURCE_COMMENT_LIST = -200162; 21 | 22 | /** 23 | * 在页面中显示评论列表 24 | * 25 | * @param context 26 | * @param mCommentList 27 | * @param commentList 28 | * @param btnComment 29 | * @param tagHandler 30 | */ 31 | public static void parseCommentList(Context context, ArrayList mCommentList, LinearLayout commentList, 32 | View btnComment, Html.TagHandler tagHandler) { 33 | if (btnComment != null) { 34 | btnComment.setTag(KEY_COMMENT_SOURCE_COMMENT_LIST, mCommentList); 35 | } 36 | TextView textView; 37 | Comment comment; 38 | int i; 39 | String content; 40 | for (i = 0; i < mCommentList.size(); i++) { 41 | comment = mCommentList.get(i); 42 | textView = (TextView) commentList.getChildAt(i); 43 | if (textView == null) { // 创建评论Videw 44 | textView = (TextView) View.inflate(context, R.layout.view_comment_list_item, null); 45 | commentList.addView(textView); 46 | } 47 | textView.setVisibility(View.VISIBLE); 48 | if (comment.mReceiver == null) { // 没有评论接受者 49 | content = String.format("<%s>%s: <%s>%s", CustomTagHandler.TAG_COMMENTATOR, 50 | comment.mCommentator.mName, CustomTagHandler.TAG_COMMENTATOR, 51 | CustomTagHandler.TAG_CONTENT, comment.mContent, CustomTagHandler.TAG_CONTENT); 52 | } else { 53 | content = String.format("<%s>%s 回复 <%s>%s: <%s>%s", 54 | CustomTagHandler.TAG_COMMENTATOR, comment.mCommentator.mName, CustomTagHandler.TAG_COMMENTATOR, 55 | CustomTagHandler.TAG_RECEIVER, comment.mReceiver.mName, CustomTagHandler.TAG_RECEIVER, 56 | CustomTagHandler.TAG_CONTENT, comment.mContent, CustomTagHandler.TAG_CONTENT); 57 | } 58 | textView.setText(Html.fromHtml(content, null, tagHandler)); // 解析标签 59 | textView.setClickable(true); 60 | textView.setMovementMethod(LinkMovementMethod.getInstance()); 61 | textView.setTag(CustomTagHandler.KEY_COMMENTATOR, comment.mCommentator); 62 | textView.setTag(CustomTagHandler.KEY_RECEIVER, comment.mReceiver); 63 | 64 | textView.setTag(KEY_COMMENT_SOURCE_COMMENT_LIST, mCommentList); 65 | } 66 | for (; i < commentList.getChildCount(); i++) { 67 | commentList.getChildAt(i).setVisibility(View.GONE); 68 | } 69 | if (mCommentList.size() > 0) { 70 | commentList.setVisibility(View.VISIBLE); 71 | } else { 72 | commentList.setVisibility(View.GONE); 73 | } 74 | } 75 | 76 | 77 | /** 78 | * 弹出评论对话框 79 | */ 80 | public static void inputComment(final Activity activity, final ListView listView, 81 | final View btnComment, final User receiver, 82 | final InputCommentListener listener) { 83 | 84 | final ArrayList commentList = (ArrayList) btnComment.getTag(KEY_COMMENT_SOURCE_COMMENT_LIST); 85 | 86 | String hint; 87 | if (receiver != null) { 88 | if (receiver.mId == MainActivity.sUser.mId) { 89 | hint = "我也说一句"; 90 | } else { 91 | hint = "回复 " + receiver.mName; 92 | } 93 | } else { 94 | hint = "我也说一句"; 95 | } 96 | // 获取评论的位置,不要在CommentDialogListener.onShow()中获取,onShow在输入法弹出后才调用, 97 | // 此时btnComment所属的父布局可能已经被ListView回收 98 | final int[] coord = new int[2]; 99 | if (listView != null) { 100 | btnComment.getLocationOnScreen(coord); 101 | } 102 | 103 | //弹出评论对话框 104 | showInputComment(activity, hint, new CommentDialogListener() { 105 | @Override 106 | public void onClickPublish(final Dialog dialog, EditText input, final TextView btn) { 107 | final String content = input.getText().toString(); 108 | if (content.trim().equals("")) { 109 | Toast.makeText(activity, "评论不能为空", Toast.LENGTH_SHORT).show(); 110 | return; 111 | } 112 | btn.setClickable(false); 113 | final long receiverId = receiver == null ? -1 : receiver.mId; 114 | Comment comment = new Comment(MainActivity.sUser, content, receiver); 115 | commentList.add(comment); 116 | if (listener != null) { 117 | listener.onCommitComment(); 118 | } 119 | dialog.dismiss(); 120 | Toast.makeText(activity, "评论成功", Toast.LENGTH_SHORT).show(); 121 | } 122 | 123 | /** 124 | * @see CommentDialogListener 125 | * @param inputViewCoordinatesInScreen [left,top] 126 | */ 127 | @Override 128 | public void onShow(int[] inputViewCoordinatesInScreen) { 129 | if (listView != null) { 130 | // 点击某条评论则这条评论刚好在输入框上面,点击评论按钮则输入框刚好挡住按钮 131 | int span = btnComment.getId() == R.id.btn_input_comment ? 0 : btnComment.getHeight(); 132 | listView.smoothScrollBy(coord[1] + span - inputViewCoordinatesInScreen[1], 1000); 133 | } 134 | } 135 | 136 | @Override 137 | public void onDismiss() { 138 | 139 | } 140 | }); 141 | 142 | } 143 | 144 | public static class InputCommentListener { 145 | // 评论成功时调用 146 | public void onCommitComment() { 147 | 148 | } 149 | } 150 | 151 | 152 | /** 153 | * 弹出评论对话框 154 | */ 155 | private static Dialog showInputComment(Activity activity, CharSequence hint, final CommentDialogListener listener) { 156 | final Dialog dialog = new Dialog(activity, android.R.style.Theme_Translucent_NoTitleBar); 157 | dialog.setContentView(R.layout.view_input_comment); 158 | dialog.findViewById(R.id.input_comment_dialog_container).setOnClickListener(new View.OnClickListener() { 159 | @Override 160 | public void onClick(View v) { 161 | dialog.dismiss(); 162 | if (listener != null) { 163 | listener.onDismiss(); 164 | } 165 | } 166 | }); 167 | final EditText input = (EditText) dialog.findViewById(R.id.input_comment); 168 | input.setHint(hint); 169 | final TextView btn = (TextView) dialog.findViewById(R.id.btn_publish_comment); 170 | btn.setOnClickListener(new View.OnClickListener() { 171 | @Override 172 | public void onClick(View v) { 173 | if (listener != null) { 174 | listener.onClickPublish(dialog, input, btn); 175 | } 176 | } 177 | }); 178 | dialog.setCancelable(true); 179 | dialog.show(); 180 | new Handler().postDelayed(new Runnable() { 181 | @Override 182 | public void run() { 183 | if (listener != null) { 184 | int[] coord = new int[2]; 185 | dialog.findViewById(R.id.input_comment_container).getLocationOnScreen(coord); 186 | // 传入 输入框距离屏幕顶部(不包括状态栏)的位置 187 | listener.onShow(coord); 188 | } 189 | } 190 | }, 300); 191 | return dialog; 192 | } 193 | 194 | /** 195 | * 评论对话框相关监听 196 | */ 197 | public interface CommentDialogListener { 198 | void onClickPublish(Dialog dialog, EditText input, TextView btn); 199 | 200 | /** 201 | * onShow在输入法弹出后才调用 202 | * @param inputViewCoordinatesOnScreen 输入框距离屏幕顶部(不包括状态栏)的位置[left,top] 203 | */ 204 | void onShow(int[] inputViewCoordinatesOnScreen); 205 | 206 | void onDismiss(); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /app/src/com/example/QzoneComment/CustomTagHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.QzoneComment; 2 | 3 | import android.content.Context; 4 | import android.text.Editable; 5 | import android.text.Html; 6 | import android.text.Spanned; 7 | import android.text.TextPaint; 8 | import android.text.style.ClickableSpan; 9 | import android.text.style.TextAppearanceSpan; 10 | import android.view.View; 11 | import com.example.QzoneComment.model.User; 12 | import org.xml.sax.XMLReader; 13 | 14 | import java.util.HashMap; 15 | 16 | /** 17 | * 显示评论的自定义Html标签解析器 18 | */ 19 | public class CustomTagHandler implements Html.TagHandler { 20 | //自定义标签 21 | public static final String TAG_COMMENTATOR = "commentator"; // 评论者 22 | public static final String TAG_RECEIVER = "receiver"; // 评论接收者,即对谁评论 23 | public static final String TAG_CONTENT = "content"; // 评论内容 24 | 25 | public static final int KEY_COMMENTATOR = -2016; 26 | public static final int KEY_RECEIVER = -20162; 27 | 28 | public static final int KEY_COMMENTATOR_START = 1; 29 | public static final int KEY_RECEIVER_START = 11; 30 | public static final int KEY_CONTENT_START = 21; 31 | 32 | private HashMap mMaps = new HashMap(); 33 | 34 | private ClickableSpan mCommentatorSpan, mReceiverSpan, mContentSpan; 35 | 36 | private Context mContext; 37 | 38 | public CustomTagHandler(final Context context, final OnCommentClickListener listener) { 39 | mContext = context; 40 | mCommentatorSpan = new BaseClickableSpan() { 41 | @Override 42 | public void onClick(View widget) { 43 | if (listener != null) { 44 | User user = (User) widget.getTag(KEY_COMMENTATOR); 45 | listener.onCommentatorClick(widget, user); 46 | } 47 | } 48 | }; 49 | mReceiverSpan = new BaseClickableSpan() { 50 | @Override 51 | public void onClick(View widget) { 52 | if (listener != null) { 53 | User user = (User) widget.getTag(KEY_RECEIVER); 54 | listener.onReceiverClick(widget, user); 55 | } 56 | } 57 | @Override 58 | public void updateDrawState(TextPaint ds) { 59 | ds.setColor(0xFF436B9C);//接收者字体颜色 60 | ds.setUnderlineText(false); 61 | } 62 | }; 63 | mContentSpan = new BaseClickableSpan() { 64 | @Override 65 | public void onClick(View widget) { 66 | if (listener != null) { 67 | User commentator = (User) widget.getTag(KEY_COMMENTATOR); 68 | User receiver = (User) widget.getTag(KEY_RECEIVER); 69 | listener.onContentClick(widget, commentator, receiver); 70 | } 71 | } 72 | 73 | @Override 74 | public void updateDrawState(TextPaint ds) { 75 | ds.setColor(0xff000000); 76 | ds.setUnderlineText(false); 77 | } 78 | }; 79 | } 80 | 81 | /** 82 | * 解析自定义标签 83 | */ 84 | @Override 85 | public void handleTag(boolean opening, String tag, final Editable output, XMLReader xmlReader) { 86 | if (!tag.toLowerCase().equals(TAG_COMMENTATOR) && !tag.toLowerCase().equals(TAG_RECEIVER) 87 | && !tag.toLowerCase().equals(TAG_CONTENT)) { 88 | return; 89 | } 90 | if (opening) { //开始标签 91 | // 记录标签内容的起始索引 92 | int mStart = output.length(); 93 | if (tag.toLowerCase().equals(TAG_COMMENTATOR)) { 94 | mMaps.put(KEY_COMMENTATOR_START, mStart); 95 | } else if (tag.toLowerCase().equals(TAG_RECEIVER)) { 96 | mMaps.put(KEY_RECEIVER_START, mStart); 97 | } else if (tag.toLowerCase().equals(TAG_CONTENT)) { 98 | mMaps.put(KEY_CONTENT_START, mStart); 99 | } 100 | } else { // 结束标签 101 | int mEnd = output.length(); //标签内容的结束索引 102 | 103 | if (tag.toLowerCase().equals(TAG_COMMENTATOR)) { 104 | int mStart = mMaps.get(KEY_COMMENTATOR_START); 105 | output.setSpan(new TextAppearanceSpan(mContext, R.style.Comment), 106 | mStart, mEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 107 | output.setSpan(mCommentatorSpan, mStart, mEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 108 | 109 | } else if (tag.toLowerCase().equals(TAG_RECEIVER)) { 110 | int mStart = mMaps.get(KEY_RECEIVER_START); 111 | output.setSpan(new TextAppearanceSpan(mContext, R.style.Comment), 112 | mStart, mEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 113 | output.setSpan(mReceiverSpan, mStart, mEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 114 | 115 | } else if (tag.toLowerCase().equals(TAG_CONTENT)) { 116 | int mStart = mMaps.get(KEY_CONTENT_START); 117 | output.setSpan(new TextAppearanceSpan(mContext, R.style.Comment), 118 | mStart, mEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 119 | output.setSpan(mContentSpan, mStart, mEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * 126 | */ 127 | abstract class BaseClickableSpan extends ClickableSpan { 128 | public BaseClickableSpan() { 129 | 130 | } 131 | 132 | @Override 133 | public abstract void onClick(View widget); 134 | 135 | @Override 136 | public void updateDrawState(TextPaint ds) { 137 | ds.setColor(ds.linkColor); //默认颜色 138 | ds.setUnderlineText(false); 139 | } 140 | } 141 | 142 | public interface OnCommentClickListener { 143 | // 点击评论者 144 | void onCommentatorClick(View view, User commentator); 145 | 146 | // 点击接收者 147 | void onReceiverClick(View view, User receiver); 148 | 149 | // 点击评论内容 150 | void onContentClick(View view, User commentator, User receiver); 151 | 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /app/src/com/example/QzoneComment/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.QzoneComment; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.*; 6 | import android.widget.AdapterView; 7 | import android.widget.ListView; 8 | import android.widget.Toast; 9 | import com.example.QzoneComment.model.Comment; 10 | import com.example.QzoneComment.model.Moment; 11 | import com.example.QzoneComment.model.User; 12 | 13 | import java.util.ArrayList; 14 | 15 | public class MainActivity extends Activity { 16 | 17 | public static User sUser = new User(1, "走向远方"); // 当前登录用户 18 | 19 | private ListView mListView; 20 | private MomentAdapter mAdapter; 21 | 22 | @Override 23 | public void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | requestWindowFeature(Window.FEATURE_NO_TITLE); 26 | // this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 27 | setContentView(R.layout.main); 28 | mListView = (ListView) findViewById(R.id.list_moment); 29 | 30 | // 模拟数据 31 | ArrayList moments = new ArrayList(); 32 | for (int i = 0; i < 20; i++) { 33 | ArrayList comments = new ArrayList(); 34 | comments.add(new Comment(new User(i + 2, "用户" + i), "评论" + i, null)); 35 | comments.add(new Comment(new User(i + 100, "用户" + (i + 100)), "评论" + i, new User(i + 200, "用户" + (i + 200)))); 36 | comments.add(new Comment(new User(i + 200, "用户" + (i + 200)), "评论" + i, null)); 37 | comments.add(new Comment(new User(i + 300, "用户" + (i + 300)), "评论" + i, null)); 38 | moments.add(new Moment("动态 " + i, comments)); 39 | } 40 | 41 | mAdapter = new MomentAdapter(this, moments, new CustomTagHandler(this, new CustomTagHandler.OnCommentClickListener() { 42 | @Override 43 | public void onCommentatorClick(View view, User commentator) { 44 | Toast.makeText(getApplicationContext(), commentator.mName, Toast.LENGTH_SHORT).show(); 45 | } 46 | 47 | @Override 48 | public void onReceiverClick(View view, User receiver) { 49 | Toast.makeText(getApplicationContext(), receiver.mName, Toast.LENGTH_SHORT).show(); 50 | } 51 | 52 | // 点击评论内容,弹出输入框回复评论 53 | @Override 54 | public void onContentClick(View view, User commentator, User receiver) { 55 | if (commentator != null && commentator.mId == sUser.mId) { // 不能回复自己的评论 56 | return; 57 | } 58 | inputComment(view, commentator); 59 | } 60 | })); 61 | 62 | mListView.setAdapter(mAdapter); 63 | 64 | mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 65 | @Override 66 | public void onItemClick(AdapterView parent, View view, int position, long id) { 67 | Toast.makeText(getApplicationContext(),"click "+position,Toast.LENGTH_SHORT).show(); 68 | } 69 | }); 70 | } 71 | 72 | public void inputComment(final View v) { 73 | inputComment(v, null); 74 | } 75 | 76 | /** 77 | * 弹出评论对话框 78 | * @param v 79 | * @param receiver 80 | */ 81 | public void inputComment(final View v, User receiver) { 82 | CommentFun.inputComment(MainActivity.this, mListView, v, receiver, new CommentFun.InputCommentListener() { 83 | @Override 84 | public void onCommitComment() { 85 | mAdapter.notifyDataSetChanged(); 86 | } 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/com/example/QzoneComment/MomentAdapter.java: -------------------------------------------------------------------------------- 1 | package com.example.QzoneComment; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.BaseAdapter; 7 | import android.widget.LinearLayout; 8 | import android.widget.TextView; 9 | import com.example.QzoneComment.model.Moment; 10 | 11 | import java.util.ArrayList; 12 | 13 | /** 14 | * Created by huangziwei on 16-4-12. 15 | */ 16 | public class MomentAdapter extends BaseAdapter { 17 | 18 | public static final int VIEW_HEADER = 0; 19 | public static final int VIEW_MOMENT = 1; 20 | 21 | 22 | private ArrayList mList; 23 | private Context mContext; 24 | private CustomTagHandler mTagHandler; 25 | 26 | public MomentAdapter(Context context, ArrayList list, CustomTagHandler tagHandler) { 27 | mList = list; 28 | mContext = context; 29 | mTagHandler = tagHandler; 30 | } 31 | 32 | @Override 33 | public int getViewTypeCount() { 34 | return 2; 35 | } 36 | 37 | @Override 38 | public int getItemViewType(int position) { 39 | return position == 0 ? VIEW_HEADER : VIEW_MOMENT; 40 | } 41 | 42 | @Override 43 | public int getCount() { 44 | // heanderView 45 | return mList.size() + 1; 46 | } 47 | 48 | @Override 49 | public Object getItem(int position) { 50 | return null; 51 | } 52 | 53 | @Override 54 | public long getItemId(int position) { 55 | return 0; 56 | } 57 | 58 | @Override 59 | public View getView(int position, View convertView, ViewGroup parent) { 60 | if (convertView == null) { 61 | if (position == 0) { 62 | convertView = View.inflate(mContext, R.layout.item_header, null); 63 | } else { 64 | convertView = View.inflate(mContext, R.layout.item_moment, null); 65 | ViewHolder holder = new ViewHolder(); 66 | holder.mCommentList = (LinearLayout) convertView.findViewById(R.id.comment_list); 67 | holder.mBtnInput = convertView.findViewById(R.id.btn_input_comment); 68 | // holder.mContent = (TextView) convertView.findViewById(R.id.content); 69 | convertView.setTag(holder); 70 | } 71 | } 72 | //防止ListView的OnItemClick与item里面子view的点击发生冲突 73 | ((ViewGroup) convertView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 74 | if (position == 0) { 75 | 76 | } else { 77 | int index = position - 1; 78 | ViewHolder holder = (ViewHolder) convertView.getTag(); 79 | // holder.mContent.setText(mList.get(index).mContent); 80 | CommentFun.parseCommentList(mContext, mList.get(index).mComment, 81 | holder.mCommentList, holder.mBtnInput, mTagHandler); 82 | } 83 | return convertView; 84 | } 85 | 86 | private static class ViewHolder { 87 | LinearLayout mCommentList; 88 | View mBtnInput; 89 | TextView mContent; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/com/example/QzoneComment/model/Comment.java: -------------------------------------------------------------------------------- 1 | package com.example.QzoneComment.model; 2 | 3 | /** 4 | * 评论对象 5 | */ 6 | public class Comment { 7 | public String mContent; // 评论内容 8 | public User mCommentator; // 评论者 9 | public User mReceiver; // 接收者(即回复谁) 10 | 11 | public Comment(User mCommentator, String mContent, User mReceiver) { 12 | this.mCommentator = mCommentator; 13 | this.mContent = mContent; 14 | this.mReceiver = mReceiver; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/com/example/QzoneComment/model/Moment.java: -------------------------------------------------------------------------------- 1 | package com.example.QzoneComment.model; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * 评论对象 7 | */ 8 | public class Moment { 9 | 10 | public String mContent; 11 | public ArrayList mComment; // 评论列表 12 | 13 | public Moment(String mContent,ArrayList mComment) { 14 | this.mComment = mComment; 15 | this.mContent = mContent; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/com/example/QzoneComment/model/User.java: -------------------------------------------------------------------------------- 1 | package com.example.QzoneComment.model; 2 | 3 | /** 4 | * 用户 5 | */ 6 | public class User { 7 | 8 | public long mId; // id 9 | public String mName; // 用户名 10 | 11 | 12 | public User(long mId, String mName) { 13 | this.mId = mId; 14 | this.mName = mName; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:2.2.3' 7 | } 8 | } 9 | 10 | allprojects { 11 | repositories { 12 | jcenter() 13 | } 14 | } 15 | 16 | task clean(type: Delete) { 17 | delete rootProject.buildDir 18 | } -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1993hzw/QZoneComment/911ccbf6760011f5da6772a5ac917a87a07dde9b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Sep 18 20:47:01 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | #Mon Sep 18 20:45:59 CST 2017 11 | sdk.dir=/Users/huangziwei/Library/Android/sdk 12 | -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | project.type=0 15 | target=android-19 16 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' --------------------------------------------------------------------------------