├── BubbleLayout ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── tc │ │ │ └── bubblelayout │ │ │ ├── BubbleGroupView.java │ │ │ ├── BubbleImageView.java │ │ │ ├── BubblePopGroupView.java │ │ │ ├── BubbleSimpleDraweeView.java │ │ │ ├── BubbleTextView.java │ │ │ ├── CpuUtil.java │ │ │ ├── DensityUtil.java │ │ │ ├── LogUtil.java │ │ │ ├── MainActivity.java │ │ │ ├── MyApplication.java │ │ │ ├── RoundCornerSimpleDraweeView.java │ │ │ ├── RoundUtil.java │ │ │ ├── SOManager.java │ │ │ ├── ShareReflectUtil.java │ │ │ ├── SharedTool.java │ │ │ ├── TinkerLoadLibrary.java │ │ │ ├── URegex.java │ │ │ ├── fresco │ │ │ ├── FrescoUtil.java │ │ │ ├── blur │ │ │ │ ├── BitmapBlurHelper.java │ │ │ │ ├── FastBlur.java │ │ │ │ └── RSBlur.java │ │ │ ├── config │ │ │ │ └── ImagePipelineConfigFactory.java │ │ │ ├── controller │ │ │ │ └── SingleImageControllerListener.java │ │ │ ├── listener │ │ │ │ ├── ILoadImageResult.java │ │ │ │ └── MyPostprocessor.java │ │ │ └── utils │ │ │ │ ├── DensityUtil.java │ │ │ │ └── StreamTool.java │ │ │ └── testrecylerview │ │ │ ├── AbstractGroupItemDecoration.java │ │ │ ├── GroupItemDecoration.java │ │ │ ├── GroupPressBackgroundDrawable.java │ │ │ ├── GroupSortItem.java │ │ │ ├── IGroupSort.java │ │ │ ├── ListAdapter.java │ │ │ ├── NormalDecoration.java │ │ │ ├── PressEffectGroupItemDecoration.java │ │ │ ├── TestGroupBean.java │ │ │ ├── TestListActivity.java │ │ │ └── readme │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ ├── pic1.png │ │ └── white_sound_wave_default.png │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_test_list.xml │ │ ├── include_pop_emoji_bubble.xml │ │ └── item_test_list.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle └── README.md /BubbleLayout/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /BubbleLayout/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /BubbleLayout/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "com.tc.bubblelayout" 7 | minSdkVersion 15 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | buildTypes { 13 | debug { 14 | ndk { abiFilters = 15 | [ 16 | "armeabi-v7a", 17 | "arm64-v8a", 18 | // "armeabi" 19 | ] 20 | 21 | } 22 | 23 | // 移除无用的resource文件 24 | shrinkResources false 25 | debuggable true 26 | minifyEnabled false 27 | } 28 | 29 | release { 30 | ndk { 31 | abiFilters = 32 | [ 33 | "armeabi-v7a", 34 | "arm64-v8a", 35 | // "armeabi" 36 | ] 37 | } 38 | 39 | // 移除无用的resource文件 40 | shrinkResources true 41 | debuggable false 42 | minifyEnabled true 43 | } 44 | } 45 | sourceSets { 46 | main { 47 | jniLibs.srcDirs = ['libs'] 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | implementation fileTree(dir: 'libs', include: ['*.jar']) 54 | implementation 'com.android.support:appcompat-v7:27.1.1' 55 | compile 'com.android.support:recyclerview-v7:27.0.0' 56 | compile 'com.facebook.fresco:fresco:1.13.0' 57 | compile 'com.facebook.fresco:animated-gif:1.13.0' 58 | compile 'com.facebook.fresco:imagepipeline-okhttp3:1.13.0' 59 | // implementation 'com.facebook.soloader:soloader:0.6.0' 60 | compile 'com.getkeepsafe.relinker:relinker:1.3.1' 61 | compile 'io.reactivex:rxjava:1.0.14' 62 | compile 'io.reactivex:rxandroid:1.0.1' 63 | } 64 | -------------------------------------------------------------------------------- /BubbleLayout/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/BubbleGroupView.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import android.graphics.PaintFlagsDrawFilter; 9 | import android.graphics.Path; 10 | import android.graphics.PointF; 11 | import android.graphics.PorterDuff; 12 | import android.graphics.PorterDuffXfermode; 13 | import android.graphics.RectF; 14 | import android.os.Build; 15 | import android.support.annotation.Nullable; 16 | import android.util.AttributeSet; 17 | import android.view.View; 18 | import android.widget.LinearLayout; 19 | 20 | /** 21 | * author: tc 22 | * date: 2018/3/14 & 10:04 23 | * version 1.0 24 | * description 透明气泡view, 25 | * 注意内部如果有TextView,文本长度要短,不能超出一屏幕,不然在列表控件里可能部分机器显示无内容、空白。 26 | * 如果遇到这种场景,可以采用BubbleTextView的写法,用clipPath做气泡犄角,不过会导致裁剪边缘出现锯齿 27 | * modify by 28 | */ 29 | public class BubbleGroupView extends LinearLayout { 30 | private Path mSrcPath; 31 | private int mHeight; 32 | private int mWidth; 33 | private Paint mPaint; 34 | private RectF mRoundRect; 35 | /** 36 | * 上弧线控制点和下弧线控制点,控制犄角的形状 37 | */ 38 | private PointF topControl, bottomControl; 39 | 40 | /** 41 | * 气泡图形留空区域宽度,影响气泡犄角的宽度 42 | */ 43 | private int mWidthDiff; 44 | /** 45 | * 左、右上角圆角的半径,影响气泡的起点 46 | */ 47 | private int mRoundRadius; 48 | /** 49 | * 是否是右侧气泡 50 | */ 51 | private boolean mIsRightPop; 52 | private PorterDuffXfermode mPorterDuffXfermode; 53 | /** 54 | * 加载时背景色 55 | */ 56 | private int mLoadingBackColor; 57 | private int mLeftTextPadding; 58 | private int mRightTextPadding; 59 | private Paint mBorderPaint; 60 | private Path mCornerPath; 61 | private PaintFlagsDrawFilter mPaintFlagsDrawFilter; 62 | private int mBorderColor; 63 | private Bitmap mBubbleBitmap; 64 | private Canvas mBubbleCanvas; 65 | private boolean mIsShowBorder; 66 | private int mDefaultCornerPadding; 67 | 68 | public BubbleGroupView(Context context) { 69 | this(context, null); 70 | } 71 | 72 | public BubbleGroupView(Context context, @Nullable AttributeSet attrs) { 73 | this(context, attrs, 0); 74 | } 75 | 76 | public BubbleGroupView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 77 | super(context, attrs, defStyleAttr); 78 | init(context, attrs); 79 | } 80 | 81 | private void init(Context context, AttributeSet attrs) { 82 | TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.BubbleView); 83 | mLoadingBackColor = attr.getColor(R.styleable.BubbleView_BubbleView_backgroundColor, 0); 84 | mIsRightPop = attr.getBoolean(R.styleable.BubbleView_BubbleView_rightPop, true); 85 | //左侧或右侧留出的空余区域 86 | mWidthDiff = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_blank_space_width, 87 | DensityUtil.dip2px(getContext(), 7)); 88 | //圆角的半径 89 | mRoundRadius = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_roundRadius, 90 | DensityUtil.dip2px(context, 8)); 91 | mLeftTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_leftTextPadding, 92 | DensityUtil.dip2px(context, 0)); 93 | mRightTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_rightTextPadding, 94 | DensityUtil.dip2px(context, 0)); 95 | attr.recycle(); 96 | 97 | mBubbleCanvas = new Canvas(); 98 | mSrcPath = new Path(); 99 | mCornerPath = new Path(); 100 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 101 | mPaint.setAntiAlias(true); 102 | mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 103 | mBorderPaint.setStyle(Paint.Style.STROKE); 104 | mBorderPaint.setAntiAlias(true); 105 | mBorderPaint.setStrokeWidth(0.5f); 106 | mBorderColor = getResources().getColor(R.color.color_999999); 107 | mBorderPaint.setColor(mBorderColor); 108 | topControl = new PointF(0, 0); 109 | bottomControl = new PointF(0, 0); 110 | mRoundRect = new RectF(); 111 | mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); 112 | mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint 113 | .FILTER_BITMAP_FLAG); 114 | setTextPadding(mRightTextPadding, mLeftTextPadding); 115 | mIsShowBorder = true; 116 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 117 | } 118 | 119 | 120 | private void initValues() { 121 | mDefaultCornerPadding = DensityUtil.dip2px(getContext(), 3); 122 | if (mIsRightPop) { 123 | //设置犄角的控制横坐标xy 124 | topControl.x = mWidth - DensityUtil.dip2px(getContext(), 2); 125 | topControl.y = mRoundRadius; 126 | bottomControl.x = mWidth - DensityUtil.dip2px(getContext(), 1); 127 | bottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 128 | } else { 129 | //设置犄角的控制横坐标xy 130 | topControl.x = DensityUtil.dip2px(getContext(), 2); 131 | topControl.y = mRoundRadius; 132 | bottomControl.x = DensityUtil.dip2px(getContext(), 1); 133 | bottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 134 | } 135 | 136 | } 137 | 138 | public void setShowBorder(boolean showBorder) { 139 | mIsShowBorder = showBorder; 140 | } 141 | 142 | public void setLoadingBackColor(int loadingBackColor) { 143 | if (loadingBackColor <= 0) { 144 | mLoadingBackColor = 0; 145 | return; 146 | } 147 | mLoadingBackColor = getResources().getColor(loadingBackColor); 148 | } 149 | 150 | 151 | public void setBorderColor(int borderColor) { 152 | if (borderColor <= 0) { 153 | mBorderColor = 0; 154 | return; 155 | } 156 | mBorderColor = getResources().getColor(borderColor); 157 | mBorderPaint.setColor(borderColor); 158 | } 159 | 160 | private void setTextPadding(int rightTextPadding, int leftTextPadding) { 161 | if (mIsRightPop) { 162 | setPadding(leftTextPadding, getPaddingTop(), rightTextPadding + mWidthDiff, 163 | getPaddingBottom()); 164 | } else { 165 | setPadding(leftTextPadding + mWidthDiff, getPaddingTop(), rightTextPadding, getPaddingBottom()); 166 | } 167 | } 168 | 169 | 170 | public void updateView() { 171 | setTextPadding(mRightTextPadding, mLeftTextPadding); 172 | invalidate(); 173 | } 174 | 175 | /** 176 | * 设置圆角的半径 177 | * 178 | * @param roundRadius 179 | */ 180 | public void setRoundRadius(int roundRadius) { 181 | mRoundRadius = DensityUtil.dip2px(getContext(), roundRadius); 182 | } 183 | 184 | 185 | /** 186 | * 是否是右侧气泡 187 | * 188 | * @param rightPop 是否是右侧气泡 false则为左侧气泡 189 | */ 190 | public void setRightPop(boolean rightPop) { 191 | mIsRightPop = rightPop; 192 | } 193 | 194 | @Override 195 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 196 | super.onSizeChanged(w, h, oldw, oldh); 197 | mHeight = h; 198 | mWidth = w; 199 | initValues(); 200 | //创建气泡布局 201 | createBubbleLayout(); 202 | 203 | } 204 | 205 | private void createBubbleLayout() { 206 | if (mBubbleBitmap != null && !mBubbleBitmap.isRecycled()) { 207 | mBubbleBitmap.recycle(); 208 | } 209 | mBubbleBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); 210 | mBubbleCanvas.setBitmap(mBubbleBitmap); 211 | drawBubblePath(mBubbleCanvas); 212 | } 213 | 214 | @Override 215 | protected void dispatchDraw(Canvas canvas) { 216 | canvas.setDrawFilter(mPaintFlagsDrawFilter); 217 | int saveCount = canvas.saveLayerAlpha(0, 0, mWidth, mHeight, 255, 218 | Canvas.ALL_SAVE_FLAG); 219 | drawBackColor(canvas); 220 | super.dispatchDraw(canvas); 221 | 222 | mPaint.setXfermode(mPorterDuffXfermode); 223 | //绘制气泡部分,和 super.onDraw(canvas);绘制的画面利用xfermode做叠加计算 224 | canvas.drawBitmap(mBubbleBitmap, 0, 0, mPaint); 225 | mPaint.setXfermode(null); 226 | canvas.restoreToCount(saveCount); 227 | if (mIsShowBorder && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mBorderColor != 0) { 228 | //绘制气泡的四周边框 229 | canvas.drawPath(mSrcPath, mBorderPaint); 230 | } 231 | } 232 | 233 | private void drawBackColor(Canvas canvas) { 234 | if (mLoadingBackColor != 0) { 235 | canvas.drawColor(mLoadingBackColor); 236 | } 237 | } 238 | 239 | /** 240 | * 计算绘制气泡 241 | */ 242 | private void drawBubblePath(Canvas canvas) { 243 | mSrcPath.reset(); 244 | mCornerPath.reset(); 245 | if (mIsRightPop) { 246 | mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight); 247 | } else { 248 | mRoundRect.set(mWidthDiff, 0, mWidth, mHeight); 249 | } 250 | 251 | mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW); 252 | 253 | if (mIsRightPop) { 254 | //给path增加右侧的犄角,形成气泡效果 255 | mCornerPath.moveTo(mWidth - mWidthDiff, mRoundRadius); 256 | mCornerPath.quadTo(topControl.x, topControl.y, mWidth, mRoundRadius - mDefaultCornerPadding); 257 | mCornerPath.quadTo(bottomControl.x, bottomControl.y, mWidth - mWidthDiff, 258 | mRoundRadius + mWidthDiff); 259 | } else { 260 | //给path增加左侧的犄角,形成气泡效果 261 | mCornerPath.moveTo(mWidthDiff, mRoundRadius); 262 | mCornerPath.quadTo(topControl.x, topControl.y, 0, mRoundRadius - mDefaultCornerPadding); 263 | mCornerPath.quadTo(bottomControl.x, bottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff); 264 | } 265 | mCornerPath.close(); 266 | //绘制path所形成的图形,清除形成透明效果,露出这一区域 267 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 268 | mSrcPath.op(mCornerPath, Path.Op.UNION); 269 | canvas.drawPath(mSrcPath, mPaint); 270 | //绘制气泡的四周边框 271 | canvas.drawPath(mSrcPath, mBorderPaint); 272 | } else { 273 | mSrcPath.addPath(mCornerPath); 274 | canvas.drawPath(mCornerPath, mPaint); 275 | } 276 | } 277 | 278 | 279 | } 280 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/BubbleImageView.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import android.graphics.PaintFlagsDrawFilter; 9 | import android.graphics.Path; 10 | import android.graphics.PointF; 11 | import android.graphics.PorterDuff; 12 | import android.graphics.PorterDuffXfermode; 13 | import android.graphics.RectF; 14 | import android.support.annotation.Nullable; 15 | import android.support.v7.widget.AppCompatImageView; 16 | import android.util.AttributeSet; 17 | import android.view.View; 18 | 19 | /** 20 | * author: tc 21 | * date: 2018/3/14 & 10:04 22 | * version 1.0 23 | * description 透明气泡view 24 | * modify by 25 | */ 26 | public class BubbleImageView extends AppCompatImageView { 27 | private Path mSrcPath; 28 | private int mHeight; 29 | private int mWidth; 30 | private Paint mPaint; 31 | private RectF mRoundRect; 32 | /** 33 | * 上弧线控制点和下弧线控制点 34 | */ 35 | private PointF topControl, bottomControl; 36 | 37 | /** 38 | * 气泡图形右侧留空区域宽度 39 | */ 40 | private int mWidthDiff; 41 | /** 42 | * 右上角圆角的半径 43 | */ 44 | private int mRoundRadius; 45 | /** 46 | * 是否是右侧气泡 47 | */ 48 | private boolean mIsRightPop; 49 | private PorterDuffXfermode mPorterDuffXfermode; 50 | private int mLeftTextPadding; 51 | private int mRightTextPadding; 52 | /** 53 | * 语音长度 54 | */ 55 | private int mVoiceLength; 56 | 57 | /** 58 | * 加载时背景色 59 | */ 60 | private int mLoadingBackColor; 61 | private int mDefaultPadding; 62 | private int mDefaultCornerPadding; 63 | private PaintFlagsDrawFilter mPaintFlagsDrawFilter; 64 | private Bitmap mBubbleBitmap; 65 | private Canvas mBubbleCanvas; 66 | 67 | public BubbleImageView(Context context) { 68 | this(context, null); 69 | } 70 | 71 | public BubbleImageView(Context context, @Nullable AttributeSet attrs) { 72 | this(context, attrs, 0); 73 | } 74 | 75 | public BubbleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 76 | super(context, attrs, defStyleAttr); 77 | init(context, attrs); 78 | } 79 | 80 | private void init(Context context, AttributeSet attrs) { 81 | TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.BubbleView); 82 | mLoadingBackColor = attr.getColor(R.styleable.BubbleView_BubbleView_backgroundColor, 0); 83 | mIsRightPop = attr.getBoolean(R.styleable.BubbleView_BubbleView_rightPop, true); 84 | //左侧或右侧留出的空余区域 85 | mWidthDiff = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_blank_space_width, 86 | DensityUtil.dip2px(getContext(), 7)); 87 | //圆角的半径 88 | mRoundRadius = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_roundRadius, 89 | DensityUtil.dip2px(context, 8)); 90 | mLeftTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_leftTextPadding, 91 | DensityUtil.dip2px(context, 0)); 92 | mRightTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_rightTextPadding, 93 | DensityUtil.dip2px(context, 0)); 94 | attr.recycle(); 95 | 96 | mSrcPath = new Path(); 97 | mBubbleCanvas = new Canvas(); 98 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 99 | topControl = new PointF(0, 0); 100 | bottomControl = new PointF(0, 0); 101 | mRoundRect = new RectF(); 102 | mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); 103 | 104 | 105 | mDefaultPadding = DensityUtil.dip2px(getContext(), 10); 106 | mDefaultCornerPadding = DensityUtil.dip2px(getContext(), 3); 107 | mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint 108 | .FILTER_BITMAP_FLAG); 109 | setTextPadding(mRightTextPadding, mLeftTextPadding); 110 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 111 | } 112 | 113 | 114 | private void initValues() { 115 | if (mIsRightPop) { 116 | //设置犄角的控制横坐标xy 117 | topControl.x = mWidth - DensityUtil.dip2px(getContext(), 2); 118 | topControl.y = mRoundRadius; 119 | bottomControl.x = mWidth - DensityUtil.dip2px(getContext(), 1); 120 | bottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 121 | } else { 122 | //设置犄角的控制横坐标xy 123 | topControl.x = DensityUtil.dip2px(getContext(), 2); 124 | topControl.y = mRoundRadius; 125 | bottomControl.x = DensityUtil.dip2px(getContext(), 1); 126 | bottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 127 | } 128 | 129 | } 130 | 131 | 132 | @Override 133 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 134 | super.onSizeChanged(w, h, oldw, oldh); 135 | mHeight = h; 136 | mWidth = w; 137 | initValues(); 138 | 139 | //创建气泡布局 140 | createBubbleLayout(); 141 | 142 | } 143 | 144 | private void createBubbleLayout() { 145 | if (mBubbleBitmap != null && !mBubbleBitmap.isRecycled()) { 146 | mBubbleBitmap.recycle(); 147 | } 148 | mBubbleBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); 149 | mBubbleCanvas.setBitmap(mBubbleBitmap); 150 | drawBubblePath(mBubbleCanvas); 151 | } 152 | 153 | public void judgePadding() { 154 | if (mVoiceLength == 1) { 155 | setTextPadding(mDefaultPadding, mDefaultPadding); 156 | } else { 157 | setTextPadding(mRightTextPadding, mLeftTextPadding); 158 | } 159 | } 160 | 161 | @Override 162 | protected void onDraw(Canvas canvas) { 163 | canvas.setDrawFilter(mPaintFlagsDrawFilter); 164 | int saveCount = canvas.saveLayerAlpha(0, 0, mWidth, mHeight, 255, 165 | Canvas.ALL_SAVE_FLAG); 166 | drawBackColor(canvas); 167 | super.onDraw(canvas); 168 | 169 | mPaint.setXfermode(mPorterDuffXfermode); 170 | //绘制气泡部分,和 super.onDraw(canvas);绘制的画面利用xfermode做叠加计算 171 | canvas.drawBitmap(mBubbleBitmap, 0, 0, mPaint); 172 | 173 | mPaint.setXfermode(null); 174 | canvas.restoreToCount(saveCount); 175 | 176 | } 177 | 178 | private void drawBackColor(Canvas canvas) { 179 | if (mLoadingBackColor != 0) { 180 | canvas.drawColor(mLoadingBackColor); 181 | } 182 | } 183 | 184 | /** 185 | * 绘制气泡路径 186 | */ 187 | private void drawBubblePath(Canvas canvas) { 188 | mSrcPath.reset(); 189 | if (mIsRightPop) { 190 | mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight); 191 | } else { 192 | mRoundRect.set(mWidthDiff, 0, mWidth, mHeight); 193 | } 194 | 195 | mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW); 196 | 197 | if (mIsRightPop) { 198 | //给path增加右侧的犄角,形成气泡效果 199 | mSrcPath.moveTo(mWidth - mWidthDiff, mRoundRadius); 200 | mSrcPath.quadTo(topControl.x, topControl.y, mWidth, mRoundRadius - mDefaultCornerPadding); 201 | mSrcPath.quadTo(bottomControl.x, bottomControl.y, mWidth - mWidthDiff, 202 | mRoundRadius + mWidthDiff); 203 | } else { 204 | //给path增加左侧的犄角,形成气泡效果 205 | mSrcPath.moveTo(mWidthDiff, mRoundRadius); 206 | mSrcPath.quadTo(topControl.x, topControl.y, 0, mRoundRadius - mDefaultCornerPadding); 207 | mSrcPath.quadTo(bottomControl.x, bottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff); 208 | } 209 | mSrcPath.close(); 210 | //绘制path所形成的图形,清除形成透明效果,露出这一区域 211 | canvas.drawPath(mSrcPath, mPaint); 212 | 213 | } 214 | 215 | private void setTextPadding(int rightTextPadding, int leftTextPadding) { 216 | if (mIsRightPop) { 217 | setPadding(leftTextPadding, getPaddingTop(), rightTextPadding + mWidthDiff, getPaddingBottom()); 218 | } else { 219 | setPadding(leftTextPadding + mWidthDiff, getPaddingTop(), rightTextPadding, getPaddingBottom()); 220 | } 221 | } 222 | 223 | public void setLoadingBackColor(int loadingBackColor) { 224 | if (loadingBackColor <= 0) { 225 | mLoadingBackColor = 0; 226 | return; 227 | } 228 | mLoadingBackColor = getResources().getColor(loadingBackColor); 229 | } 230 | 231 | public void setLeftTextPadding(int leftTextPadding) { 232 | mLeftTextPadding = DensityUtil.dip2px(getContext(), leftTextPadding); 233 | } 234 | 235 | public void setRightTextPadding(int rightTextPadding) { 236 | mRightTextPadding = DensityUtil.dip2px(getContext(), rightTextPadding); 237 | } 238 | 239 | public void updateView() { 240 | judgePadding(); 241 | invalidate(); 242 | } 243 | 244 | /** 245 | * 设置圆角的半径 246 | * 247 | * @param roundRadius 248 | */ 249 | public void setRoundRadius(int roundRadius) { 250 | mRoundRadius = DensityUtil.dip2px(getContext(), roundRadius); 251 | } 252 | 253 | 254 | public void setLength(int length) { 255 | this.mVoiceLength = length; 256 | } 257 | 258 | /** 259 | * 是否是右侧气泡 260 | * 261 | * @param rightPop 是否是右侧气泡 false则为左侧气泡 262 | */ 263 | public void setRightPop(boolean rightPop) { 264 | mIsRightPop = rightPop; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/BubblePopGroupView.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.Paint; 9 | import android.graphics.PaintFlagsDrawFilter; 10 | import android.graphics.Path; 11 | import android.graphics.PorterDuff; 12 | import android.graphics.PorterDuffXfermode; 13 | import android.graphics.RectF; 14 | import android.os.Build; 15 | import android.support.annotation.Nullable; 16 | import android.util.AttributeSet; 17 | import android.view.View; 18 | import android.widget.FrameLayout; 19 | 20 | /** 21 | * author: tc 22 | * date: 2018/3/14 & 10:04 23 | * version 1.0 24 | * description 透明气泡view,尖角在下方的气泡view,调用show方法会直接悬浮到界面decorview里的顶层 25 | * 注意内部如果有TextView,文本长度要短,不能超出一屏幕,不然在列表控件里可能部分机器显示无内容、空白。 26 | * 如果遇到这种场景,可以采用BubbleTextView的写法,用clipPath做气泡犄角,不过会导致裁剪边缘出现锯齿 27 | * modify by 28 | */ 29 | public class BubblePopGroupView extends FrameLayout { 30 | private static final String TAG = "BubbleBottomGroupView"; 31 | private Path mSrcPath; 32 | private int mHeight; 33 | private int mWidth; 34 | private Paint mPaint; 35 | private RectF mRoundRect; 36 | 37 | /** 38 | * 左侧或右侧尖角下,留出的空余区域,尖角离左右边的距离, 39 | */ 40 | private int mWidthDiff; 41 | /** 42 | * 左、右上角圆角的半径,影响气泡的起点 43 | */ 44 | private int mRoundRadius; 45 | private PorterDuffXfermode mPorterDuffXfermode; 46 | /** 47 | * 加载时背景色 48 | */ 49 | private int mLoadingBackColor; 50 | private int mLeftTextPadding; 51 | private int mRightTextPadding; 52 | private Paint mBorderPaint; 53 | private Path mCornerPath; 54 | private PaintFlagsDrawFilter mPaintFlagsDrawFilter; 55 | private int mBorderColor; 56 | private Bitmap mBubbleBitmap; 57 | private Canvas mBubbleCanvas; 58 | private boolean mIsShowBorder; 59 | private int mBubbleHeight; 60 | private int mLocation; 61 | private boolean mIsDismiss; 62 | 63 | public BubblePopGroupView(Context context) { 64 | this(context, null); 65 | } 66 | 67 | public BubblePopGroupView(Context context, @Nullable AttributeSet attrs) { 68 | this(context, attrs, 0); 69 | } 70 | 71 | public BubblePopGroupView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 72 | super(context, attrs, defStyleAttr); 73 | init(context, attrs); 74 | } 75 | 76 | private void init(Context context, AttributeSet attrs) { 77 | TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.BubbleView); 78 | mLoadingBackColor = attr.getColor(R.styleable.BubbleView_BubbleView_backgroundColor, 0); 79 | //左侧或右侧留出的空余区域 80 | mWidthDiff = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_blank_space_width, 81 | DensityUtil.dip2px(getContext(), 28)); 82 | //圆角的半径 83 | mRoundRadius = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_roundRadius, 84 | DensityUtil.dip2px(context, 10)); 85 | mLeftTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_leftTextPadding, 86 | DensityUtil.dip2px(context, 0)); 87 | mRightTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_rightTextPadding, 88 | DensityUtil.dip2px(context, 0)); 89 | attr.recycle(); 90 | mBubbleCanvas = new Canvas(); 91 | mSrcPath = new Path(); 92 | mCornerPath = new Path(); 93 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 94 | mPaint.setAntiAlias(true); 95 | mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 96 | mBorderPaint.setStyle(Paint.Style.STROKE); 97 | mBorderPaint.setAntiAlias(true); 98 | mBorderPaint.setStrokeWidth(0.5f); 99 | mBorderColor = getResources().getColor(R.color.color_999999); 100 | mBorderPaint.setColor(mBorderColor); 101 | mRoundRect = new RectF(); 102 | mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); 103 | mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint 104 | .FILTER_BITMAP_FLAG); 105 | mBubbleHeight = DensityUtil.dip2px(getContext(), 13); 106 | setTextPadding(mRightTextPadding, mLeftTextPadding); 107 | mIsShowBorder = true; 108 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 109 | } 110 | 111 | 112 | public void setShowBorder(boolean showBorder) { 113 | mIsShowBorder = showBorder; 114 | } 115 | 116 | public void setLoadingBackColor(int loadingBackColor) { 117 | if (loadingBackColor <= 0) { 118 | mLoadingBackColor = 0; 119 | return; 120 | } 121 | mLoadingBackColor = getResources().getColor(loadingBackColor); 122 | } 123 | 124 | 125 | public void setBorderColor(int borderColor) { 126 | if (borderColor <= 0) { 127 | mBorderColor = 0; 128 | return; 129 | } 130 | mBorderColor = getResources().getColor(borderColor); 131 | mBorderPaint.setColor(borderColor); 132 | } 133 | 134 | private void setTextPadding(int rightTextPadding, int leftTextPadding) { 135 | setPadding(leftTextPadding, getPaddingTop(), rightTextPadding, 136 | getPaddingBottom() + mBubbleHeight); 137 | } 138 | 139 | 140 | public void updateView() { 141 | createBubbleLayout(); 142 | setTextPadding(mRightTextPadding, mLeftTextPadding); 143 | invalidate(); 144 | } 145 | 146 | /** 147 | * 设置圆角的半径 148 | * 149 | * @param roundRadius 150 | */ 151 | public void setRoundRadius(int roundRadius) { 152 | mRoundRadius = DensityUtil.dip2px(getContext(), roundRadius); 153 | } 154 | 155 | 156 | /** 157 | * location,0,1,2分别为左侧、中间、右侧尖角 158 | */ 159 | public void setLocation(int location) { 160 | mLocation = location; 161 | } 162 | 163 | @Override 164 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 165 | super.onSizeChanged(w, h, oldw, oldh); 166 | mHeight = h; 167 | mWidth = w; 168 | 169 | } 170 | 171 | private void createBubbleLayout() { 172 | if (mBubbleBitmap != null && !mBubbleBitmap.isRecycled()) { 173 | mBubbleBitmap.recycle(); 174 | } 175 | if (mWidth == 0 || mHeight == 0) { 176 | LogUtil.w(TAG, "mWidth==0||mHeight==0"); 177 | return; 178 | } 179 | mBubbleBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); 180 | mBubbleCanvas.setBitmap(mBubbleBitmap); 181 | drawBubblePath(mBubbleCanvas); 182 | } 183 | 184 | @Override 185 | protected void dispatchDraw(Canvas canvas) { 186 | //创建气泡布局 187 | createBubbleLayout(); 188 | canvas.setDrawFilter(mPaintFlagsDrawFilter); 189 | int saveCount = canvas.saveLayerAlpha(0, 0, mWidth, mHeight, 255, 190 | Canvas.ALL_SAVE_FLAG); 191 | drawBackColor(canvas); 192 | super.dispatchDraw(canvas); 193 | 194 | mPaint.setXfermode(mPorterDuffXfermode); 195 | //绘制气泡部分,和 super.onDraw(canvas);绘制的画面利用xfermode做叠加计算 196 | canvas.drawBitmap(mBubbleBitmap, 0, 0, mPaint); 197 | mPaint.setXfermode(null); 198 | canvas.restoreToCount(saveCount); 199 | if (mIsShowBorder && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mBorderColor != 0) { 200 | //绘制气泡的四周边框 201 | canvas.drawPath(mSrcPath, mBorderPaint); 202 | } 203 | } 204 | 205 | private void drawBackColor(Canvas canvas) { 206 | if (mLoadingBackColor != 0) { 207 | canvas.drawColor(mLoadingBackColor); 208 | } 209 | } 210 | 211 | /** 212 | * 计算绘制气泡 213 | */ 214 | private void drawBubblePath(Canvas canvas) { 215 | mSrcPath.reset(); 216 | mCornerPath.reset(); 217 | mRoundRect.set(0, 0, mWidth, mHeight - mBubbleHeight); 218 | 219 | mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW); 220 | //尖角的宽度 221 | int popWidth = DensityUtil.dip2px(getContext(), 26); 222 | if (mLocation == 2) { 223 | //给path增加右侧的犄角,形成气泡效果 224 | mCornerPath.moveTo(mWidth - mWidthDiff, mHeight - mBubbleHeight); 225 | mCornerPath.lineTo(mWidth - mWidthDiff - popWidth / 2, mHeight); 226 | mCornerPath.lineTo(mWidth - mWidthDiff - popWidth, mHeight - mBubbleHeight); 227 | } else if (mLocation == 1) { 228 | //给path增加中间的犄角,形成气泡效果 229 | int startPos = mWidth / 2 - popWidth / 2; 230 | mCornerPath.moveTo(startPos, mHeight - mBubbleHeight); 231 | mCornerPath.lineTo(startPos + popWidth / 2, mHeight); 232 | mCornerPath.lineTo(startPos + popWidth, mHeight - mBubbleHeight); 233 | } else { 234 | //给path增加左侧的犄角,形成气泡效果 235 | mCornerPath.moveTo(mWidthDiff, mHeight - mBubbleHeight); 236 | mCornerPath.lineTo(mWidthDiff + popWidth / 2, mHeight); 237 | mCornerPath.lineTo(mWidthDiff + popWidth, mHeight - mBubbleHeight); 238 | } 239 | mCornerPath.close(); 240 | //绘制path所形成的图形,清除形成透明效果,露出这一区域 241 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 242 | mSrcPath.op(mCornerPath, Path.Op.UNION); 243 | canvas.drawPath(mSrcPath, mPaint); 244 | //绘制气泡的四周边框 245 | canvas.drawPath(mSrcPath, mBorderPaint); 246 | } else { 247 | mSrcPath.addPath(mCornerPath); 248 | canvas.drawPath(mCornerPath, mPaint); 249 | } 250 | } 251 | 252 | /** 253 | * 计算指定的anchor View 在屏幕中的坐标。 254 | */ 255 | private RectF calcViewScreenLocation(View anchor) { 256 | int[] location = new int[2]; 257 | // 获取控件在屏幕中的位置,返回的数组分别为控件左顶点的 x、y 的值 258 | anchor.getLocationOnScreen(location); 259 | return new RectF(location[0], location[1], location[0] + anchor.getWidth(), 260 | location[1] + anchor.getHeight()); 261 | } 262 | 263 | public boolean isDismiss() { 264 | return mIsDismiss; 265 | } 266 | 267 | /** 268 | * 隐藏气泡 269 | */ 270 | public void hide() { 271 | Activity context = (Activity) (getContext()); 272 | FrameLayout decorView = (FrameLayout) context.getWindow().getDecorView(); 273 | decorView.removeView(this); 274 | mIsDismiss = true; 275 | } 276 | 277 | 278 | /** 279 | * 显示气泡 280 | * 281 | * @param activity 282 | * @param anchor 以谁为参照物 283 | * @param width 气泡整体大小,宽度 284 | * @param height 气泡整体高度 285 | */ 286 | public void show(final Activity activity, final View anchor, int width, int height) { 287 | hide(); 288 | final View view = this; 289 | FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView(); 290 | 291 | RectF rectF = calcViewScreenLocation(anchor); 292 | width = DensityUtil.dip2px(getContext(), width); 293 | height = DensityUtil.dip2px(getContext(), height); 294 | FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height); 295 | int marginTop = (int) (rectF.top - layoutParams.height); 296 | int marginLeft = (int) (rectF.left - (layoutParams.width - anchor.getWidth()) / 2); 297 | marginTop = marginTop <= 0 ? 0 : marginTop; 298 | int location = 0; 299 | int displayWidth = DensityUtil.getDisplayWidth(getContext()); 300 | if (marginLeft < 0) { 301 | //如果锚点view的左间距不够显示气泡 302 | location = 0; 303 | marginLeft = DensityUtil.dip2px(getContext(), 14); 304 | } else if (displayWidth < width + marginLeft) { 305 | //如果屏幕宽度不够显示当前左间距下的气泡 306 | location = 2; 307 | marginLeft = displayWidth - DensityUtil.dip2px(getContext(), 14) - width; 308 | } else { 309 | //尖角显示在中间 310 | location = 1; 311 | } 312 | setLocation(location); 313 | layoutParams.setMargins(marginLeft, marginTop, layoutParams.rightMargin, layoutParams.bottomMargin); 314 | view.setLayoutParams(layoutParams); 315 | decorView.addView(view); 316 | mIsDismiss = false; 317 | } 318 | 319 | 320 | } 321 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/BubbleSimpleDraweeView.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.Canvas; 8 | import android.graphics.Paint; 9 | import android.graphics.PaintFlagsDrawFilter; 10 | import android.graphics.Path; 11 | import android.graphics.PointF; 12 | import android.graphics.PorterDuff; 13 | import android.graphics.PorterDuffXfermode; 14 | import android.graphics.RectF; 15 | import android.support.annotation.Nullable; 16 | import android.util.AttributeSet; 17 | import android.view.View; 18 | 19 | import com.facebook.drawee.view.SimpleDraweeView; 20 | 21 | /** 22 | * author: tc 23 | * date: 2018/3/14 & 10:04 24 | * version 1.0 25 | * description 透明气泡view 26 | * modify by 27 | */ 28 | public class BubbleSimpleDraweeView extends SimpleDraweeView { 29 | private Path mSrcPath ; 30 | private int mHeight; 31 | private int mWidth; 32 | private Paint mPaint; 33 | private RectF mRoundRect; 34 | /** 35 | * 上弧线控制点和下弧线控制点 36 | */ 37 | private PointF topControl, bottomControl; 38 | 39 | /** 40 | * 气泡图形右侧留空区域宽度 41 | */ 42 | private int mWidthDiff; 43 | /** 44 | * 右上角圆角的半径 45 | */ 46 | private int mRoundRadius; 47 | /** 48 | * 是否是右侧气泡 49 | */ 50 | private boolean mIsRightPop; 51 | private PorterDuffXfermode mPorterDuffXfermode; 52 | private Bitmap mShowButtonBitmap; 53 | /** 54 | * 加载时背景色 55 | */ 56 | private int mLoadingBackColor; 57 | private PaintFlagsDrawFilter mPaintFlagsDrawFilter; 58 | private Bitmap mBubbleBitmap; 59 | private Canvas mBubbleCanvas; 60 | private int mDefaultCornerPadding; 61 | 62 | public BubbleSimpleDraweeView(Context context) { 63 | this(context, null); 64 | } 65 | 66 | public BubbleSimpleDraweeView(Context context, @Nullable AttributeSet attrs) { 67 | this(context, attrs, 0); 68 | } 69 | 70 | public BubbleSimpleDraweeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 71 | super(context, attrs, defStyleAttr); 72 | init(context, attrs); 73 | } 74 | 75 | private void init(Context context, @Nullable AttributeSet attrs) { 76 | TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.BubbleView); 77 | mLoadingBackColor = attr.getColor(R.styleable.BubbleView_BubbleView_backgroundColor, 0); 78 | mIsRightPop = attr.getBoolean(R.styleable.BubbleView_BubbleView_rightPop, true); 79 | //左侧或右侧留出的空余区域 80 | mWidthDiff = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_blank_space_width, 81 | DensityUtil.dip2px(getContext(), 7)); 82 | //圆角的半径 83 | mRoundRadius = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_roundRadius, 84 | DensityUtil.dip2px(context, 8)); 85 | attr.recycle(); 86 | mSrcPath = new Path(); 87 | mBubbleCanvas = new Canvas(); 88 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 89 | topControl = new PointF(0, 0); 90 | bottomControl = new PointF(0, 0); 91 | mRoundRect = new RectF(); 92 | mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); 93 | mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint 94 | .FILTER_BITMAP_FLAG); 95 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 96 | 97 | } 98 | 99 | 100 | private void initValues() { 101 | mDefaultCornerPadding = DensityUtil.dip2px(getContext(), 3); 102 | if (mIsRightPop) { 103 | //设置犄角的控制横坐标xy 104 | topControl.x = mWidth - DensityUtil.dip2px(getContext(), 2); 105 | topControl.y = mRoundRadius; 106 | bottomControl.x = mWidth - DensityUtil.dip2px(getContext(), 1); 107 | bottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 108 | } else { 109 | //设置犄角的控制横坐标xy 110 | topControl.x = DensityUtil.dip2px(getContext(), 2); 111 | topControl.y = mRoundRadius; 112 | bottomControl.x = DensityUtil.dip2px(getContext(), 1); 113 | bottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 114 | } 115 | } 116 | 117 | public void setLoadingBackColor(int loadingBackColor) { 118 | if (loadingBackColor <= 0) { 119 | mLoadingBackColor = 0; 120 | return; 121 | } 122 | mLoadingBackColor = getResources().getColor(loadingBackColor); 123 | } 124 | 125 | /** 126 | * 设置要展示的图片提示按钮的图片drawable值 127 | * 128 | * @param showButtonImg 129 | */ 130 | public void setShowButtonImg(int showButtonImg) { 131 | int showButtonImg1 = showButtonImg; 132 | mShowButtonBitmap = BitmapFactory.decodeResource(getResources(), showButtonImg1); 133 | } 134 | 135 | /** 136 | * 把右下角要展示的图片重置,不再显示 137 | */ 138 | public void resetShowButtonImg() { 139 | mShowButtonBitmap = null; 140 | } 141 | 142 | public void updateView() { 143 | invalidate(); 144 | } 145 | 146 | /** 147 | * 设置圆角的半径 148 | * 149 | * @param roundRadius 150 | */ 151 | public void setRoundRadius(int roundRadius) { 152 | mRoundRadius = DensityUtil.dip2px(getContext(), roundRadius); 153 | } 154 | 155 | 156 | /** 157 | * 是否是右侧气泡 158 | * 159 | * @param rightPop 是否是右侧气泡 false则为左侧气泡 160 | */ 161 | public void setRightPop(boolean rightPop) { 162 | mIsRightPop = rightPop; 163 | } 164 | 165 | @Override 166 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 167 | super.onSizeChanged(w, h, oldw, oldh); 168 | mHeight = h; 169 | mWidth = w; 170 | initValues(); 171 | //创建气泡布局 172 | createBubbleLayout(); 173 | 174 | } 175 | 176 | private void createBubbleLayout() { 177 | if (mBubbleBitmap != null && !mBubbleBitmap.isRecycled()) { 178 | mBubbleBitmap.recycle(); 179 | } 180 | mBubbleBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); 181 | mBubbleCanvas.setBitmap(mBubbleBitmap); 182 | drawBubblePath(mBubbleCanvas); 183 | } 184 | 185 | 186 | @Override 187 | protected void onDraw(Canvas canvas) { 188 | canvas.setDrawFilter(mPaintFlagsDrawFilter); 189 | int saveCount = canvas.saveLayerAlpha(0, 0, mWidth, mHeight, 255, 190 | Canvas.ALL_SAVE_FLAG); 191 | drawBackColor(canvas); 192 | super.onDraw(canvas); 193 | drawTipIcon(canvas); 194 | 195 | mPaint.setXfermode(mPorterDuffXfermode); 196 | //绘制气泡部分,和 super.onDraw(canvas);绘制的画面利用xfermode做叠加计算 197 | canvas.drawBitmap(mBubbleBitmap, 0, 0, mPaint); 198 | 199 | mPaint.setXfermode(null); 200 | canvas.restoreToCount(saveCount); 201 | } 202 | 203 | private void drawTipIcon(Canvas canvas) { 204 | if (mShowButtonBitmap != null) { 205 | int bitmapHeight = mShowButtonBitmap.getHeight(); 206 | int bitmapWidth = mShowButtonBitmap.getWidth(); 207 | int top = mHeight - bitmapHeight - DensityUtil.dip2px(getContext(), 5); 208 | if (mIsRightPop) { 209 | canvas.drawBitmap(mShowButtonBitmap, DensityUtil.dip2px 210 | (getContext(), 5), top, mPaint); 211 | } else { 212 | canvas.drawBitmap(mShowButtonBitmap, mWidth - bitmapWidth - DensityUtil.dip2px 213 | (getContext(), 5), top, mPaint); 214 | } 215 | } 216 | } 217 | 218 | private void drawBackColor(Canvas canvas) { 219 | if (mLoadingBackColor != 0) { 220 | canvas.drawColor(mLoadingBackColor); 221 | } 222 | } 223 | 224 | /** 225 | * 绘制气泡路径 226 | */ 227 | private void drawBubblePath(Canvas canvas) { 228 | mSrcPath.reset(); 229 | if (mIsRightPop) { 230 | mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight); 231 | } else { 232 | mRoundRect.set(mWidthDiff, 0, mWidth, mHeight); 233 | } 234 | 235 | mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW); 236 | 237 | if (mIsRightPop) { 238 | //给path增加右侧的犄角,形成气泡效果 239 | mSrcPath.moveTo(mWidth - mWidthDiff, mRoundRadius); 240 | mSrcPath.quadTo(topControl.x, topControl.y, mWidth, mRoundRadius - mDefaultCornerPadding); 241 | mSrcPath.quadTo(bottomControl.x, bottomControl.y, mWidth - mWidthDiff, 242 | mRoundRadius + mWidthDiff); 243 | } else { 244 | //给path增加左侧的犄角,形成气泡效果 245 | mSrcPath.moveTo(mWidthDiff, mRoundRadius); 246 | mSrcPath.quadTo(topControl.x, topControl.y, 0, mRoundRadius - mDefaultCornerPadding); 247 | mSrcPath.quadTo(bottomControl.x, bottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff); 248 | } 249 | mSrcPath.close(); 250 | //绘制path所形成的图形,清除形成透明效果,露出这一区域 251 | canvas.drawPath(mSrcPath, mPaint); 252 | 253 | } 254 | 255 | 256 | } 257 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/BubbleTextView.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.graphics.PaintFlagsDrawFilter; 8 | import android.graphics.Path; 9 | import android.graphics.PointF; 10 | import android.graphics.RectF; 11 | import android.support.annotation.Nullable; 12 | import android.support.v7.widget.AppCompatTextView; 13 | import android.util.AttributeSet; 14 | 15 | /** 16 | * author: tc 17 | * date: 2018/3/14 & 10:04 18 | * version 1.0 19 | * description 透明气泡view 20 | * modify by 21 | */ 22 | public class BubbleTextView extends AppCompatTextView { 23 | private static final String TAG = "BubbleTextView"; 24 | private Path mSrcPath; 25 | private int mHeight; 26 | private int mWidth; 27 | private RectF mRoundRect; 28 | /** 29 | * 上弧线控制点和下弧线控制点 30 | */ 31 | private PointF mTopControl, mBottomControl; 32 | 33 | /** 34 | * 气泡图形右侧留空区域宽度 35 | */ 36 | private int mWidthDiff; 37 | /** 38 | * 右上角圆角的半径 39 | */ 40 | private int mRoundRadius; 41 | /** 42 | * 是否是右侧气泡 43 | */ 44 | private boolean mIsRightPop; 45 | private int mLeftTextPadding; 46 | private int mRightTextPadding; 47 | 48 | /** 49 | * 加载时背景色 50 | */ 51 | private int mLoadingBackColor; 52 | private int mDefaultPadding; 53 | private int mDefaultCornerPadding; 54 | private PaintFlagsDrawFilter mPaintFlagsDrawFilter; 55 | 56 | public BubbleTextView(Context context) { 57 | this(context, null); 58 | } 59 | 60 | public BubbleTextView(Context context, @Nullable AttributeSet attrs) { 61 | this(context, attrs, 0); 62 | } 63 | 64 | public BubbleTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 65 | super(context, attrs, defStyleAttr); 66 | init(context, attrs); 67 | } 68 | 69 | private void init(Context context, AttributeSet attrs) { 70 | TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.BubbleView); 71 | mLoadingBackColor = attr.getColor(R.styleable.BubbleView_BubbleView_backgroundColor, 0); 72 | mIsRightPop = attr.getBoolean(R.styleable.BubbleView_BubbleView_rightPop, true); 73 | //左侧或右侧留出的空余区域 74 | mWidthDiff = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_blank_space_width, 75 | DensityUtil.dip2px(getContext(), 7)); 76 | //圆角的半径 77 | mRoundRadius = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_roundRadius, 78 | DensityUtil.dip2px(context, 8)); 79 | mLeftTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_leftTextPadding, 80 | DensityUtil.dip2px(context, 0)); 81 | mRightTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_rightTextPadding, 82 | DensityUtil.dip2px(context, 0)); 83 | attr.recycle(); 84 | mSrcPath = new Path(); 85 | mTopControl = new PointF(0, 0); 86 | mBottomControl = new PointF(0, 0); 87 | mRoundRect = new RectF(); 88 | //默认一个字的时候的间隔 89 | mDefaultPadding = DensityUtil.dip2px(getContext(), 16); 90 | mDefaultCornerPadding = DensityUtil.dip2px(getContext(), 3); 91 | mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint 92 | .FILTER_BITMAP_FLAG); 93 | setTextPadding(mRightTextPadding, mLeftTextPadding); 94 | } 95 | 96 | 97 | private void initValues() { 98 | if (mIsRightPop) { 99 | //设置犄角的控制横坐标xy 100 | mTopControl.x = mWidth - DensityUtil.dip2px(getContext(), 2); 101 | mTopControl.y = mRoundRadius; 102 | mBottomControl.x = mWidth - DensityUtil.dip2px(getContext(), 1); 103 | mBottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 104 | } else { 105 | //设置犄角的控制横坐标xy 106 | mTopControl.x = DensityUtil.dip2px(getContext(), 2); 107 | mTopControl.y = mRoundRadius; 108 | mBottomControl.x = DensityUtil.dip2px(getContext(), 1); 109 | mBottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6); 110 | } 111 | 112 | } 113 | 114 | 115 | @Override 116 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 117 | super.onSizeChanged(w, h, oldw, oldh); 118 | mHeight = h; 119 | mWidth = w; 120 | initValues(); 121 | } 122 | 123 | public void judgePadding() { 124 | int length = getText().length(); 125 | if (length == 1) { 126 | setTextPadding(mDefaultPadding, mDefaultPadding); 127 | } else { 128 | setTextPadding(mRightTextPadding, mLeftTextPadding); 129 | } 130 | } 131 | 132 | 133 | @Override 134 | protected void onDraw(Canvas canvas) { 135 | canvas.setDrawFilter(mPaintFlagsDrawFilter); 136 | // LogUtil.i(TAG, getText() + " getPaddingLeft" + getPaddingLeft() + " getPaddingRight" + getPaddingRight()); 137 | mSrcPath.reset(); 138 | if (mIsRightPop) { 139 | mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight); 140 | mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW); 141 | //给path增加右侧的犄角,形成气泡效果 142 | mSrcPath.moveTo(mWidth - mWidthDiff, mRoundRadius); 143 | mSrcPath.quadTo(mTopControl.x, mTopControl.y, mWidth, mRoundRadius - mDefaultCornerPadding); 144 | mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidth - mWidthDiff, 145 | mRoundRadius + mWidthDiff); 146 | } else { 147 | mRoundRect.set(mWidthDiff, 0, mWidth, mHeight); 148 | mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW); 149 | //给path增加左侧的犄角,形成气泡效果 150 | mSrcPath.moveTo(mWidthDiff, mRoundRadius); 151 | mSrcPath.quadTo(mTopControl.x, mTopControl.y, 0, mRoundRadius - mDefaultCornerPadding); 152 | mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff); 153 | } 154 | canvas.clipPath(mSrcPath); 155 | if (mLoadingBackColor != 0) { 156 | canvas.drawColor(mLoadingBackColor); 157 | } 158 | super.onDraw(canvas); 159 | 160 | } 161 | 162 | private void setTextPadding(int rightTextPadding, int leftTextPadding) { 163 | if (mIsRightPop) { 164 | setPadding(leftTextPadding, getPaddingTop(), rightTextPadding + mWidthDiff, getPaddingBottom()); 165 | } else { 166 | setPadding(leftTextPadding + mWidthDiff, getPaddingTop(), rightTextPadding, getPaddingBottom()); 167 | } 168 | } 169 | 170 | public void setLoadingBackColor(int loadingBackColor) { 171 | if (loadingBackColor <= 0) { 172 | mLoadingBackColor = 0; 173 | return; 174 | } 175 | mLoadingBackColor = getResources().getColor(loadingBackColor); 176 | } 177 | 178 | public void setLeftTextPadding(int leftTextPadding) { 179 | mLeftTextPadding = DensityUtil.dip2px(getContext(), leftTextPadding); 180 | } 181 | 182 | public void setRightTextPadding(int rightTextPadding) { 183 | mRightTextPadding = DensityUtil.dip2px(getContext(), rightTextPadding); 184 | } 185 | 186 | public void updateView() { 187 | judgePadding(); 188 | invalidate(); 189 | } 190 | 191 | /** 192 | * 设置圆角的半径 193 | * 194 | * @param roundRadius 195 | */ 196 | public void setRoundRadius(int roundRadius) { 197 | mRoundRadius = DensityUtil.dip2px(getContext(), roundRadius); 198 | } 199 | 200 | 201 | /** 202 | * 是否是右侧气泡 203 | * 204 | * @param rightPop 是否是右侧气泡 false则为左侧气泡 205 | */ 206 | public void setRightPop(boolean rightPop) { 207 | mIsRightPop = rightPop; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/CpuUtil.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.lang.reflect.Method; 9 | import java.util.Locale; 10 | 11 | /** 12 | * author: tc 13 | * date: 2019/6/14 & 10:17 14 | * version 1.0 15 | * description 16 | * modify by 17 | */ 18 | public class CpuUtil { 19 | public static final String CPU_ARCHITECTURE_TYPE_32 = "32"; 20 | public static final String CPU_ARCHITECTURE_TYPE_64 = "64"; 21 | private static final String TAG = "CpuUtil"; 22 | /** 23 | * ELF文件头 e_indent[]数组文件类标识索引 24 | */ 25 | private static final int EI_CLASS = 4; 26 | /** 27 | * ELF文件头 e_indent[EI_CLASS]的取值:ELFCLASS32表示32位目标 28 | */ 29 | private static final int ELFCLASS32 = 1; 30 | /** 31 | * ELF文件头 e_indent[EI_CLASS]的取值:ELFCLASS64表示64位目标 32 | */ 33 | private static final int ELFCLASS64 = 2; 34 | 35 | /** 36 | * The system property key of CPU arch type 37 | */ 38 | private static final String CPU_ARCHITECTURE_KEY_64 = "ro.product.cpu.abilist64"; 39 | 40 | /** 41 | * The system libc.so file path 42 | */ 43 | private static final String SYSTEM_LIB_C_PATH = "/system/lib/libc.so"; 44 | private static final String SYSTEM_LIB_C_PATH_64 = "/system/lib64/libc.so"; 45 | private static final String PROC_CPU_INFO_PATH = "/proc/cpuinfo"; 46 | 47 | 48 | /** 49 | * Check if the CPU architecture is x86 50 | */ 51 | public static boolean checkIfCPUx86() { 52 | //1. Check CPU architecture: arm or x86 53 | if (getSystemProperty("ro.product.cpu.abi", "arm").contains("x86")) { 54 | //The CPU is x86 55 | return true; 56 | } else { 57 | return false; 58 | } 59 | } 60 | 61 | /** 62 | * Get the CPU arch type: x32 or x64 63 | */ 64 | public static String getArchType() { 65 | String type; 66 | if (getSystemProperty(CPU_ARCHITECTURE_KEY_64, "").length() > 0) { 67 | type = CPU_ARCHITECTURE_TYPE_64; 68 | } else if (isCPUInfo64()) { 69 | type = CPU_ARCHITECTURE_TYPE_64; 70 | } else if (isLibc64()) { 71 | type = CPU_ARCHITECTURE_TYPE_64; 72 | } else { 73 | type = CPU_ARCHITECTURE_TYPE_32; 74 | } 75 | LogUtil.i(TAG, "Phone cpu type:" + type); 76 | return type; 77 | } 78 | 79 | private static String getSystemProperty(String key, String defaultValue) { 80 | String value = defaultValue; 81 | try { 82 | Class clazz = Class.forName("android.os.SystemProperties"); 83 | Method get = clazz.getMethod("get", String.class, String.class); 84 | value = (String) (get.invoke(clazz, key, "")); 85 | } catch (Exception e) { 86 | LogUtil.d(TAG, "key = " + key + ", error = " + e.getMessage()); 87 | } 88 | 89 | LogUtil.d(TAG, key + " = " + value); 90 | return value; 91 | } 92 | 93 | /** 94 | * Read the first line of "/proc/cpuinfo" file, and check if it is 64 bit. 95 | */ 96 | private static boolean isCPUInfo64() { 97 | File cpuInfo = new File(PROC_CPU_INFO_PATH); 98 | if (cpuInfo != null && cpuInfo.exists()) { 99 | InputStream inputStream = null; 100 | BufferedReader bufferedReader = null; 101 | try { 102 | inputStream = new FileInputStream(cpuInfo); 103 | bufferedReader = new BufferedReader(new InputStreamReader(inputStream), 512); 104 | String line = bufferedReader.readLine(); 105 | if (line != null && line.length() > 0 && line.toLowerCase(Locale.US).contains("arch64")) { 106 | LogUtil.d(TAG, PROC_CPU_INFO_PATH + " contains is arch64"); 107 | return true; 108 | } else { 109 | LogUtil.d(TAG, PROC_CPU_INFO_PATH + " is not arch64"); 110 | } 111 | } catch (Throwable t) { 112 | LogUtil.d(TAG, "read " + PROC_CPU_INFO_PATH + " error = " + t.getMessage()); 113 | } finally { 114 | try { 115 | if (bufferedReader != null) { 116 | bufferedReader.close(); 117 | } 118 | } catch (Exception e) { 119 | LogUtil.e(TAG, e); 120 | } 121 | 122 | try { 123 | if (inputStream != null) { 124 | inputStream.close(); 125 | } 126 | } catch (Exception e) { 127 | LogUtil.e(TAG, e); 128 | } 129 | } 130 | } 131 | return false; 132 | } 133 | 134 | /** 135 | * Check if system libc.so is 32 bit or 64 bit 136 | */ 137 | private static boolean isLibc64() { 138 | File libcFile = new File(SYSTEM_LIB_C_PATH); 139 | if (libcFile != null && libcFile.exists()) { 140 | byte[] header = readELFHeaderIndentArray(libcFile); 141 | if (header != null && header[EI_CLASS] == ELFCLASS64) { 142 | LogUtil.d(TAG, SYSTEM_LIB_C_PATH + " is 64bit"); 143 | return true; 144 | } 145 | } 146 | File libcFile64 = new File(SYSTEM_LIB_C_PATH_64); 147 | if (libcFile64 != null && libcFile64.exists()) { 148 | byte[] header = readELFHeaderIndentArray(libcFile64); 149 | if (header != null && header[EI_CLASS] == ELFCLASS64) { 150 | LogUtil.d(TAG, SYSTEM_LIB_C_PATH_64 + " is 64bit"); 151 | return true; 152 | } 153 | } 154 | 155 | return false; 156 | } 157 | 158 | /** 159 | * ELF文件头格式是固定的:文件开始是一个16字节的byte数组e_indent[16] 160 | * e_indent[4]的值可以判断ELF是32位还是64位 161 | */ 162 | private static byte[] readELFHeaderIndentArray(File libFile) { 163 | if (libFile != null && libFile.exists()) { 164 | FileInputStream inputStream = null; 165 | try { 166 | inputStream = new FileInputStream(libFile); 167 | if (inputStream != null) { 168 | byte[] tempBuffer = new byte[16]; 169 | int count = inputStream.read(tempBuffer, 0, 16); 170 | if (count == 16) { 171 | return tempBuffer; 172 | } else { 173 | LogUtil.e(TAG, "Error: e_indent length should be 16, but " + 174 | "actual is " + count); 175 | } 176 | } 177 | } catch (Throwable t) { 178 | LogUtil.e(TAG, "Error:" + t.toString()); 179 | } finally { 180 | if (inputStream != null) { 181 | try { 182 | inputStream.close(); 183 | } catch (Exception e) { 184 | LogUtil.e(TAG, e); 185 | } 186 | } 187 | } 188 | } 189 | 190 | return null; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/DensityUtil.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * 密度转换工具类 7 | */ 8 | public class DensityUtil { 9 | public static int getDisplayHeight(Context context) { 10 | return context.getResources().getDisplayMetrics().heightPixels; 11 | } 12 | 13 | public static int getDisplayWidth(Context context) { 14 | return context.getResources().getDisplayMetrics().widthPixels; 15 | } 16 | /** 17 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素) 18 | * @param density 密度 19 | * @param dpValue dp 20 | * @return px 21 | */ 22 | public static int dip2px(final float density, final float dpValue) { 23 | return (int) (dpValue * density + 0.5f); 24 | } 25 | 26 | /** 27 | * 根据手机的分辨率从 px(像素) 的单位 转成为 dp 28 | * @param density 密度 29 | * @param pxValue px 30 | * @return dp 31 | */ 32 | public static int px2dip(final float density, final float pxValue) { 33 | return (int) (pxValue / density + 0.5f); 34 | } 35 | 36 | /** 37 | * 38 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素) 39 | * @param context 上下文 40 | * @param dpValue dp 41 | * @return px 42 | */ 43 | public static int dip2px(Context context, final float dpValue) { 44 | float density = context.getResources().getDisplayMetrics().density; 45 | return (int) (dpValue * density + 0.5f); 46 | } 47 | 48 | /** 49 | * 根据手机的分辨率从 px(像素) 的单位 转成为 dp 50 | * @param context 上下文 51 | * @param pxValue px 52 | * @return dp 53 | */ 54 | public static int px2dip(Context context, final float pxValue) { 55 | float density = context.getResources().getDisplayMetrics().density; 56 | return (int) (pxValue / density + 0.5f); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/LogUtil.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * author: tc 7 | * date: 2019/3/14 & 15:40 8 | * version 1.0 9 | * description 10 | * modify by 11 | */ 12 | public class LogUtil { 13 | public static void v(String tag, String info) { 14 | Log.v(tag, info); 15 | } 16 | 17 | public static void d(String tag, String info) { 18 | Log.d(tag, info); 19 | } 20 | 21 | public static void w(String tag, String info) { 22 | Log.w(tag, info); 23 | } 24 | 25 | public static void i(String tag, String info) { 26 | Log.i(tag, info); 27 | } 28 | 29 | public static void e(String tag, String info) { 30 | Log.e(tag, info); 31 | } 32 | 33 | public static void e(String tag, Throwable throwable) { 34 | Log.e(tag, "", throwable); 35 | } 36 | 37 | public static void e(String tag, String info, Throwable throwable) { 38 | Log.e(tag, info, throwable); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.net.Uri; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.util.Pair; 7 | import android.view.LayoutInflater; 8 | import android.widget.FrameLayout; 9 | 10 | import com.facebook.drawee.backends.pipeline.Fresco; 11 | import com.facebook.drawee.drawable.ScalingUtils; 12 | import com.facebook.drawee.generic.GenericDraweeHierarchy; 13 | import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; 14 | import com.facebook.drawee.interfaces.DraweeController; 15 | import com.facebook.drawee.view.SimpleDraweeView; 16 | import com.facebook.imagepipeline.request.ImageRequest; 17 | import com.facebook.imagepipeline.request.ImageRequestBuilder; 18 | import com.facebook.soloader.SysUtil; 19 | 20 | import rx.Subscriber; 21 | 22 | public class MainActivity extends AppCompatActivity { 23 | 24 | private static final String TAG = "MainActivity"; 25 | private int mRightEntranceMode; 26 | private final int ENTRANCE_CHAT_LOCATION = 0x001; 27 | private final int ENTRANCE_MEDIA = 0x010; 28 | private final int ENTRANCE_PHOTO = 0x100; 29 | /** 30 | * 入口全部需要显示 31 | */ 32 | private final int ENTRANCE_MORE = 0x111; 33 | private final int ENTRANCE_NONE = 0; 34 | 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | String[] supportedAbis = SysUtil.getSupportedAbis(); 41 | for (String supportedAbi : supportedAbis) { 42 | LogUtil.i(TAG, "supportedAbi:" + supportedAbi); 43 | } 44 | LogUtil.d(TAG, "cpu is :" + CpuUtil.getArchType()); 45 | //测试动态下载so文件,用来研究so文件动态下载拷贝,减轻apk大小,对应的目录文件需要提前拷贝到手机,模拟下载完成后的场景 46 | // testLoadSO(); 47 | testFresco(); 48 | 49 | 50 | } 51 | 52 | 53 | 54 | private void testLoadSO() { 55 | //把测试so的文件拷贝到对应目录 56 | SOManager.getInstance().copyAndInitSoFileToSystem(getApplicationContext(), "fresco", new Subscriber() { 57 | @Override 58 | public void onCompleted() { 59 | runOnUiThread(new Runnable() { 60 | @Override 61 | public void run() { 62 | LogUtil.i(TAG, "testFresco"); 63 | testFresco(); 64 | } 65 | }); 66 | } 67 | 68 | @Override 69 | public void onError(Throwable e) { 70 | LogUtil.e(TAG, e); 71 | } 72 | 73 | @Override 74 | public void onNext(Pair pair) { 75 | 76 | } 77 | }); 78 | SOManager.getInstance().copyAndInitSoFileToSystem(getApplicationContext(), "shortvideo", new Subscriber 79 | () { 80 | @Override 81 | public void onCompleted() { 82 | runOnUiThread(new Runnable() { 83 | @Override 84 | public void run() { 85 | LogUtil.i(TAG, "shortvideo"); 86 | System.loadLibrary("pldroid_amix"); 87 | // ReLinker.loadLibrary(getApplicationContext(),"pldroid_amix"); 88 | } 89 | }); 90 | } 91 | 92 | @Override 93 | public void onError(Throwable e) { 94 | LogUtil.e(TAG, e); 95 | } 96 | 97 | @Override 98 | public void onNext(Pair pair) { 99 | 100 | } 101 | }); 102 | 103 | 104 | } 105 | 106 | 107 | private void testFresco() { 108 | Uri uri = Uri.parse("https://timgsa.baidu" + 109 | ".com/timg?image&quality=80&size=b9999_10000&sec=1536753048164&di=83b9c0277f5ca3df0f214becc465527c" + 110 | "&imgtype=0&src=http%3A%2F%2Fpic150.nipic.com%2Ffile%2F20171222%2F21540071_162503708000_2.jpg"); 111 | final SimpleDraweeView sdv2 = findViewById(R.id.sdv_img); 112 | loadGIFImg(uri,sdv2); 113 | 114 | sdv2.post(new Runnable() { 115 | @Override 116 | public void run() { 117 | FrameLayout decorView = (FrameLayout) getWindow().getDecorView(); 118 | final BubblePopGroupView bubblePopGroupView = (BubblePopGroupView) LayoutInflater.from(MainActivity 119 | .this) 120 | .inflate(R.layout.include_pop_emoji_bubble, null); 121 | bubblePopGroupView.setLoadingBackColor(R.color.color_e6ffffff); 122 | bubblePopGroupView.setBorderColor(R.color.color_e6cbcbcb); 123 | bubblePopGroupView.setShowBorder(true); 124 | final RoundCornerSimpleDraweeView sdvPopImg = (RoundCornerSimpleDraweeView) bubblePopGroupView 125 | .findViewById(R.id.sdv_pop_img); 126 | sdvPopImg.setLoadingBackColor(R.color.color_e6ffffff); 127 | sdvPopImg.setRoundRadius(10); 128 | bubblePopGroupView.show(MainActivity.this, sdv2, 161, 161); 129 | final Uri uri = Uri.parse("https://timgsa.baidu" + 130 | ".com/timg?image&quality=80&size=b9999_10000&sec=1538980461934&di" + 131 | "=06bc2dc85608f9124869a640b3724332&imgtype=0&src=http%3A%2F%2Fs9.rr.itc" + 132 | ".cn%2Fr%2FwapChange%2F20171_31_11%2Fa8debe8737775787542.gif"); 133 | 134 | loadGIFImg(uri, sdvPopImg); 135 | bubblePopGroupView.updateView(); 136 | } 137 | }); 138 | } 139 | 140 | private void loadGIFImg(Uri path, SimpleDraweeView simpleDraweeView) { 141 | ImageRequest request = ImageRequestBuilder.newBuilderWithSource(path) 142 | .setProgressiveRenderingEnabled(true) 143 | .setAutoRotateEnabled(true) 144 | .build(); 145 | GenericDraweeHierarchy hierarchy = 146 | new GenericDraweeHierarchyBuilder(getApplicationContext().getResources()) 147 | .setPlaceholderImageScaleType(ScalingUtils.ScaleType.CENTER_CROP) 148 | .setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP) 149 | .build(); 150 | simpleDraweeView.setHierarchy(hierarchy); 151 | 152 | DraweeController controller = Fresco.newDraweeControllerBuilder() 153 | .setImageRequest(request) 154 | .setAutoPlayAnimations(true) 155 | .build(); 156 | simpleDraweeView.setController(controller); 157 | } 158 | 159 | 160 | } 161 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.app.Application; 4 | 5 | import com.tc.bubblelayout.fresco.FrescoUtil; 6 | 7 | public class MyApplication extends Application { 8 | @Override 9 | public void onCreate() { 10 | super.onCreate(); 11 | FrescoUtil.init(this); 12 | } 13 | } -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/RoundCornerSimpleDraweeView.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import android.graphics.PaintFlagsDrawFilter; 9 | import android.graphics.Path; 10 | import android.graphics.PorterDuff; 11 | import android.graphics.PorterDuffXfermode; 12 | import android.graphics.RectF; 13 | import android.support.annotation.Nullable; 14 | import android.util.AttributeSet; 15 | import android.view.View; 16 | 17 | import com.facebook.drawee.view.SimpleDraweeView; 18 | 19 | /** 20 | * author: tc 21 | * date: 2018/3/14 & 10:04 22 | * version 1.0 23 | * description 圆角view,用于gif圆角裁剪,fresco不支持gif圆角 24 | * modify by 25 | */ 26 | public class RoundCornerSimpleDraweeView extends SimpleDraweeView { 27 | private Path mSrcPath ; 28 | private int mHeight; 29 | private int mWidth; 30 | private Paint mPaint; 31 | private RectF mRoundRect; 32 | 33 | /** 34 | * 右上角圆角的半径 35 | */ 36 | private int mRoundRadius; 37 | private PorterDuffXfermode mPorterDuffXfermode; 38 | /** 39 | * 加载时背景色 40 | */ 41 | private int mLoadingBackColor; 42 | private PaintFlagsDrawFilter mPaintFlagsDrawFilter; 43 | private Bitmap mBubbleBitmap; 44 | private Canvas mBubbleCanvas; 45 | 46 | public RoundCornerSimpleDraweeView(Context context) { 47 | this(context, null); 48 | } 49 | 50 | public RoundCornerSimpleDraweeView(Context context, @Nullable AttributeSet attrs) { 51 | this(context, attrs, 0); 52 | } 53 | 54 | public RoundCornerSimpleDraweeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 55 | super(context, attrs, defStyleAttr); 56 | init(context, attrs); 57 | } 58 | 59 | private void init(Context context, @Nullable AttributeSet attrs) { 60 | TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.BubbleView); 61 | mLoadingBackColor = attr.getColor(R.styleable.BubbleView_BubbleView_backgroundColor, 0); 62 | //圆角的半径 63 | mRoundRadius = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_roundRadius, 64 | DensityUtil.dip2px(context, 8)); 65 | attr.recycle(); 66 | mSrcPath = new Path(); 67 | mBubbleCanvas = new Canvas(); 68 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 69 | mRoundRect = new RectF(); 70 | mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); 71 | mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint 72 | .FILTER_BITMAP_FLAG); 73 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 74 | 75 | } 76 | 77 | 78 | public void setLoadingBackColor(int loadingBackColor) { 79 | if (loadingBackColor <= 0) { 80 | mLoadingBackColor = 0; 81 | return; 82 | } 83 | mLoadingBackColor = getResources().getColor(loadingBackColor); 84 | } 85 | 86 | 87 | 88 | public void updateView() { 89 | invalidate(); 90 | } 91 | 92 | /** 93 | * 设置圆角的半径 94 | * 95 | * @param roundRadius 96 | */ 97 | public void setRoundRadius(int roundRadius) { 98 | mRoundRadius = DensityUtil.dip2px(getContext(), roundRadius); 99 | } 100 | 101 | 102 | 103 | 104 | @Override 105 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 106 | super.onSizeChanged(w, h, oldw, oldh); 107 | mHeight = h; 108 | mWidth = w; 109 | //创建气泡布局 110 | createBubbleLayout(); 111 | 112 | } 113 | 114 | private void createBubbleLayout() { 115 | if (mBubbleBitmap != null && !mBubbleBitmap.isRecycled()) { 116 | mBubbleBitmap.recycle(); 117 | } 118 | mBubbleBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); 119 | mBubbleCanvas.setBitmap(mBubbleBitmap); 120 | drawBubblePath(mBubbleCanvas); 121 | } 122 | 123 | 124 | @Override 125 | protected void onDraw(Canvas canvas) { 126 | canvas.setDrawFilter(mPaintFlagsDrawFilter); 127 | int saveCount = canvas.saveLayerAlpha(0, 0, mWidth, mHeight, 255, 128 | Canvas.ALL_SAVE_FLAG); 129 | drawBackColor(canvas); 130 | super.onDraw(canvas); 131 | 132 | mPaint.setXfermode(mPorterDuffXfermode); 133 | //绘制气泡部分,和 super.onDraw(canvas);绘制的画面利用xfermode做叠加计算 134 | canvas.drawBitmap(mBubbleBitmap, 0, 0, mPaint); 135 | 136 | mPaint.setXfermode(null); 137 | canvas.restoreToCount(saveCount); 138 | } 139 | 140 | 141 | private void drawBackColor(Canvas canvas) { 142 | if (mLoadingBackColor != 0) { 143 | canvas.drawColor(mLoadingBackColor); 144 | } 145 | } 146 | 147 | /** 148 | * 绘制气泡路径 149 | */ 150 | private void drawBubblePath(Canvas canvas) { 151 | mSrcPath.reset(); 152 | mRoundRect.set(0, 0, mWidth, mHeight); 153 | mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW); 154 | 155 | //绘制path所形成的图形,清除形成透明效果,露出这一区域 156 | canvas.drawPath(mSrcPath, mPaint); 157 | 158 | } 159 | 160 | 161 | } 162 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/RoundUtil.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import java.math.BigDecimal; 4 | 5 | /** 6 | * author: tc 7 | * date: 2017/3/2 & 下午4:54 8 | * version 1.0 9 | * description 用来四舍五入计算的类 10 | * modify by 11 | */ 12 | public final class RoundUtil { 13 | // 默认除法运算精度 14 | private static final int DEF_DIV_SCALE = 2; 15 | 16 | // 这个类不能实例化 17 | private RoundUtil() { 18 | } 19 | 20 | /** 21 | * 提供精确的加法运算。 22 | * 23 | * @param v1 被加数 24 | * @param v2 加数 25 | * @return 两个参数的和 26 | */ 27 | public static double add(double v1, double v2) { 28 | BigDecimal b1 = new BigDecimal(Double.toString(v1)); 29 | BigDecimal b2 = new BigDecimal(Double.toString(v2)); 30 | return b1.add(b2).doubleValue(); 31 | } 32 | 33 | /** 34 | * 提供精确的减法运算。 35 | * 36 | * @param v1 被减数 37 | * @param v2 减数 38 | * @return 两个参数的差 39 | */ 40 | public static double sub(double v1, double v2) { 41 | BigDecimal b1 = new BigDecimal(Double.toString(v1)); 42 | BigDecimal b2 = new BigDecimal(Double.toString(v2)); 43 | return b1.subtract(b2).doubleValue(); 44 | } 45 | 46 | /** 47 | * 提供精确的乘法运算。 48 | * 49 | * @param v1 被乘数 50 | * @param v2 乘数 51 | * @return 两个参数的积 52 | */ 53 | public static double mul(double v1, double v2) { 54 | BigDecimal b1 = new BigDecimal(Double.toString(v1)); 55 | BigDecimal b2 = new BigDecimal(Double.toString(v2)); 56 | return b1.multiply(b2).doubleValue(); 57 | } 58 | 59 | /** 60 | * 提供(相对)精确的除法运算,当发生除不尽的情况时, 61 | * 精确到小数点以后10位,以后的数字四舍五入。 62 | * 63 | * @param v1 被除数 64 | * @param v2 除数 65 | * @return 两个参数的商 66 | */ 67 | public static double div(double v1, double v2) { 68 | return div(v1, v2, DEF_DIV_SCALE); 69 | } 70 | 71 | /** 72 | * 提供(相对)精确的除法运算。 73 | * 当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入。 74 | * 75 | * @param v1 被除数 76 | * @param v2 除数 77 | * @param scale 表示表示需要精确到小数点以后几位。 78 | * @return 两个参数的商 79 | */ 80 | public static double div(double v1, double v2, int scale) { 81 | if (scale < 0) { 82 | throw new IllegalArgumentException( 83 | "The scale must be a positive integer or zero"); 84 | } 85 | BigDecimal b1 = new BigDecimal(Double.toString(v1)); 86 | BigDecimal b2 = new BigDecimal(Double.toString(v2)); 87 | return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); 88 | } 89 | 90 | /** 91 | * 提供精确的小数位四舍五入处理。 92 | * 93 | * @param v 需要四舍五入的数字 94 | * @param scale 小数点后保留几位 95 | * @return 四舍五入后的结果 96 | */ 97 | public static double round(double v, int scale) { 98 | if (scale < 0) { 99 | throw new IllegalArgumentException( 100 | "The scale must be a positive integer or zero"); 101 | } 102 | BigDecimal b = new BigDecimal(Double.toString(v)); 103 | BigDecimal one = new BigDecimal("1"); 104 | return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); 105 | } 106 | 107 | /** 108 | * 提供精确的小数位四舍五入处理。-- 109 | * 110 | * @param v 需要四舍五入的数字 111 | * @param scale 小数点后保留几位 112 | * @return 四舍五入后的结果--返回int值,用于部分模块计算折扣百分比 113 | */ 114 | public static int roundReturnInt(double v, int scale) { 115 | if (scale < 0) { 116 | return 0; 117 | } 118 | BigDecimal b = new BigDecimal(Double.toString(v)); 119 | BigDecimal one = new BigDecimal("1"); 120 | return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).intValue(); 121 | } 122 | 123 | /** 124 | * 提供精确的小数位四舍五入处理。-- 125 | * 126 | * @param v 需要四舍五入的数字 127 | * @param scale 小数点后保留几位 128 | * @return 四舍五入后的结果--返回int值,用于部分模块计算折扣百分比 129 | */ 130 | public static float roundReturnFloat(double v, int scale) { 131 | if (scale < 0) { 132 | return 0; 133 | } 134 | BigDecimal b = new BigDecimal(Double.toString(v)); 135 | BigDecimal one = new BigDecimal("1"); 136 | return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).floatValue(); 137 | } 138 | } -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/SOManager.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.os.Environment; 6 | import android.text.TextUtils; 7 | import android.util.Pair; 8 | 9 | import com.facebook.soloader.SysUtil; 10 | 11 | import java.io.Closeable; 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.FileNotFoundException; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.util.Arrays; 18 | import java.util.Locale; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | 21 | import rx.Observable; 22 | import rx.Subscriber; 23 | import rx.android.schedulers.AndroidSchedulers; 24 | import rx.schedulers.Schedulers; 25 | 26 | /** 27 | * author: tc 28 | * date: 2019/4/25 & 10:28 29 | * version 1.0 30 | * description 动态加载SO库 31 | * modify by 32 | */ 33 | public class SOManager { 34 | 35 | private static final String TAG = "SOManager"; 36 | public static final String LIBS_DIR_NAME = "libs_"; 37 | private final ConcurrentHashMap mInitModuleSOMap; 38 | 39 | private SOManager() { 40 | mInitModuleSOMap = new ConcurrentHashMap<>(10); 41 | } 42 | 43 | private static class SingleInstance { 44 | private static final SOManager INSTANCE = new SOManager(); 45 | } 46 | 47 | public static SOManager getInstance() { 48 | return SingleInstance.INSTANCE; 49 | } 50 | 51 | 52 | public void setInitSuccess(String moduleName, boolean initSuccess) { 53 | mInitModuleSOMap.put(moduleName, initSuccess); 54 | } 55 | 56 | /** 57 | * 加载 so 文件(直接指定你so下载的路径即可) 58 | * 59 | * @param context 60 | * @param soModuleName so对应的模块名 61 | */ 62 | public void copyAndInitSoFileToSystem(final Context context, final String soModuleName) { 63 | copyAndInitSoFileToSystem(context, soModuleName, null); 64 | } 65 | 66 | /** 67 | * 加载 so 文件(直接指定你so下载的路径即可) 68 | * 69 | * @param context 70 | * @param soModuleName so对应的模块名 71 | */ 72 | public void copyAndInitSoFileToSystem(final Context context, final String soModuleName, Subscriber sub) { 73 | 74 | Observable objectObservable = Observable.create(new Observable.OnSubscribe() { 75 | @Override 76 | public void call(Subscriber subscriber) { 77 | final String fromPath = Environment.getExternalStorageDirectory().getPath() + "/test_so/" + 78 | soModuleName; 79 | 80 | String soFilePath = ""; 81 | try { 82 | LogUtil.i(TAG, "copyAndInitSoFileToSystem"); 83 | String[] supportedAbis = SysUtil.getSupportedAbis(); 84 | LogUtil.i(TAG, "[copySo] Build.CPU_ABI supported api:" + Build.CPU_ABI + " second:" + Build 85 | .CPU_ABI2); 86 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 87 | LogUtil.i(TAG, "[copySo] supported api:" + Arrays.asList(Build.SUPPORTED_64_BIT_ABIS) + 88 | " " + Arrays.asList(Build.SUPPORTED_32_BIT_ABIS)); 89 | } 90 | 91 | boolean isArm64 = false; 92 | // TODO: 2019/4/28 这里要从服务器动态获取armeabi 和armeabi-v7a、armabi-v8a等对应文件。测试代码暂时默认获取v7a 93 | for (String supportedAbi : supportedAbis) { 94 | LogUtil.i(TAG, "supportedAbi:" + supportedAbi); 95 | if (Build.CPU_ABI.equals(supportedAbi)) { 96 | if ("arm64-v8a".equals(Build.CPU_ABI)) { 97 | isArm64 = true; 98 | } 99 | } 100 | } 101 | //用来区分abi拷贝记录,识别是否需要进行拷贝当前so文件到内部存储的系统目录里 102 | String abiName; 103 | if (isArm64) { 104 | abiName = "arm64-v8a"; 105 | soFilePath = fromPath + "/arm64-v8a"; 106 | } else { 107 | abiName = "armeabi-v7a"; 108 | soFilePath = fromPath + "/armeabi-v7a"; 109 | } 110 | File newSOLibPath = context.getDir(LIBS_DIR_NAME + soModuleName, Context.MODE_PRIVATE); 111 | LogUtil.i(TAG, "local cache SO lib source path:" + soFilePath + " , new SO lib source path:" 112 | + newSOLibPath.getPath()); 113 | int result = copySOToSystemDir(soFilePath, newSOLibPath.getAbsolutePath(), context, abiName, 114 | soModuleName); 115 | if (result == -1) { 116 | setInitSuccess(soModuleName, false); 117 | subscriber.onNext(new Pair<>(soModuleName, false)); 118 | LogUtil.e(TAG, "init SO lib file fail:" + soModuleName); 119 | subscriber.onCompleted(); 120 | return; 121 | } 122 | //反射把刚才的SO存放目录添加到SO加载目录列表里 123 | TinkerLoadLibrary.installNativeLibraryPath(context.getClassLoader(), newSOLibPath); 124 | //初始化SO目录完成,可以通知应用层加载对应模块数据 125 | setInitSuccess(soModuleName, true); 126 | subscriber.onNext(new Pair<>(soModuleName, true)); 127 | LogUtil.i(TAG, "init SO lib file success:" + soModuleName); 128 | } catch (Throwable e) { 129 | setInitSuccess(soModuleName, false); 130 | subscriber.onNext(new Pair<>(soModuleName, false)); 131 | LogUtil.e(TAG, "load SO lib file fail", e); 132 | subscriber.onNext(new Pair<>(soModuleName, false)); 133 | } 134 | 135 | 136 | subscriber.onCompleted(); 137 | } 138 | }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); 139 | if (sub != null) { 140 | objectObservable.subscribe(sub); 141 | } else { 142 | objectObservable.subscribe(new Subscriber() { 143 | @Override 144 | public void onCompleted() { 145 | } 146 | 147 | @Override 148 | public void onError(Throwable e) { 149 | LogUtil.e(TAG, e); 150 | } 151 | 152 | @Override 153 | public void onNext(Pair o) { 154 | 155 | } 156 | }); 157 | } 158 | 159 | } 160 | 161 | 162 | /** 163 | * @param fromPath 指定的下载目录 164 | * @param toPath 应用的包路径,最终识别SO文件的目录 165 | */ 166 | private static int copySOToSystemDir(String fromPath, String toPath, Context context, String abiName, String 167 | soModuleName) { 168 | //要复制的文件目录 169 | File root = new File(fromPath); 170 | //如同判断SD卡是否存在或者文件是否存在,如果不存在则 return出去 171 | if (!root.exists()) { 172 | LogUtil.e(TAG, "SO root file is not exist,fromPath:" + fromPath); 173 | return -1; 174 | } 175 | //如果存在则获取当前目录下的全部文件 填充数组 176 | File[] currentFiles = root.listFiles(); 177 | 178 | //目标目录 179 | File targetDir = new File(toPath); 180 | //创建目录 181 | if (!targetDir.exists()) { 182 | targetDir.mkdirs(); 183 | } 184 | LogUtil.i(TAG, String.format(Locale.ENGLISH, "abiName:%s ,soModuleName:%s ,SO root files:%d", abiName, 185 | soModuleName, currentFiles.length)); 186 | if (currentFiles != null && currentFiles.length > 0) { 187 | //遍历要复制该目录下的全部文件 188 | for (File currentFile : currentFiles) { 189 | if (currentFile.isDirectory()) { 190 | //如果当前项为子目录 进行递归 191 | copySOToSystemDir(currentFile.getPath() + "/", toPath + currentFile.getName() + "/", context, 192 | abiName, soModuleName); 193 | } else { 194 | String copyFilePath = toPath + File.separator + currentFile.getName(); 195 | 196 | // TODO: 2019/4/26 根据版本号进行变更,动态替换 197 | //如果当前项为文件则进行文件拷贝 198 | if (currentFile.getName().contains(".so")) { 199 | SharedTool instance = SharedTool.getInstance(context); 200 | //上次更新so的时间 201 | long updateSOTime = URegex.convertLong(instance.getString(currentFile.getPath() + abiName)); 202 | //最终拷贝到系统目录的so文件 203 | File soCopyFile = new File(copyFilePath); 204 | if (soCopyFile.exists() && updateSOTime == currentFile.lastModified()) { 205 | LogUtil.w(TAG, String.format(Locale.ENGLISH, "abiName:%s ,soModuleName:%s ,had copy to " + 206 | "system Dir:%s", abiName, soModuleName, currentFile.getName())); 207 | } else { 208 | createOrExistsFile(copyFilePath); 209 | boolean copyFileSuccess = copyFile(currentFile.getPath(), copyFilePath); 210 | //记录下修改so的时间 211 | instance.saveString(currentFile.getPath() + abiName, String.valueOf(currentFile 212 | .lastModified())); 213 | LogUtil.i(TAG, String.format(Locale.ENGLISH, "current SO file time " + 214 | ":%d ,record updateSOTime :%d .start copy to system Dir:%s " + 215 | " ,copy file command result:%s , abiName:%s , soModuleName:%s", 216 | currentFile.lastModified(), updateSOTime, currentFile.getName(), copyFileSuccess, 217 | abiName, soModuleName)); 218 | } 219 | 220 | } 221 | } 222 | } 223 | } 224 | return 0; 225 | } 226 | 227 | /** 228 | * 判断目录是否存在,不存在则判断是否创建成功 229 | * 230 | * @param file 文件 231 | * @return {@code true}: 存在或创建成功
{@code false}: 不存在或创建失败 232 | */ 233 | public static boolean createOrExistsDir(File file) { 234 | // 如果存在,是目录则返回true,是文件则返回false,不存在则返回是否创建成功 235 | return file != null && (file.exists() ? file.isDirectory() : file.mkdirs()); 236 | } 237 | 238 | /** 239 | * 判断文件是否存在,不存在则判断是否创建成功 240 | * 241 | * @param file 文件 242 | * @return {@code true}: 存在或创建成功
{@code false}: 不存在或创建失败 243 | */ 244 | public static boolean createOrExistsFile(File file) { 245 | if (file == null) { 246 | return false; 247 | } 248 | // 如果存在,是文件则返回true,是目录则返回false 249 | if (file.exists()) { 250 | return file.isFile(); 251 | } 252 | if (!createOrExistsDir(file.getParentFile())) { 253 | return false; 254 | } 255 | try { 256 | return file.createNewFile(); 257 | } catch (IOException e) { 258 | e.printStackTrace(); 259 | return false; 260 | } 261 | } 262 | 263 | /** 264 | * 判断文件是否存在,不存在则判断是否创建成功 265 | * 266 | * @param filePath 文件路径 267 | * @return {@code true}: 存在或创建成功
{@code false}: 不存在或创建失败 268 | */ 269 | public static boolean createOrExistsFile(String filePath) { 270 | return createOrExistsFile(getFileByPath(filePath)); 271 | } 272 | 273 | /** 274 | * 根据文件路径获取文件 275 | * 276 | * @param filePath 文件路径 277 | * @return 文件 278 | */ 279 | public static File getFileByPath(String filePath) { 280 | return TextUtils.isEmpty(filePath) ? null : new File(filePath); 281 | } 282 | 283 | /** 284 | * 判断文件或者文件夹是否存在 285 | * 286 | * @param dirPath 文件或文件夹绝对路径 287 | * @return 288 | */ 289 | public static boolean isFileExists(String dirPath) { 290 | File file = new File(dirPath); 291 | return file.exists(); 292 | } 293 | 294 | 295 | public static boolean copyFile(String oldPath, String newPath) { 296 | if (!isFileExists(oldPath)) { 297 | LogUtil.e(TAG, "this file is not exist:" + oldPath); 298 | return false; 299 | } 300 | if (!isFileExists(newPath)) { 301 | LogUtil.e(TAG, "this file is not exist:" + newPath); 302 | return false; 303 | } 304 | FileInputStream inputStream = null; 305 | FileOutputStream outputStream = null; 306 | try { 307 | inputStream = new FileInputStream(oldPath); 308 | outputStream = new FileOutputStream(newPath); 309 | byte[] buffer = new byte[1024]; 310 | while ((inputStream.read(buffer)) != -1) { 311 | outputStream.write(buffer, 0, buffer.length); 312 | } 313 | outputStream.flush(); 314 | return true; 315 | } catch (FileNotFoundException e) { 316 | LogUtil.e(TAG, e); 317 | } catch (IOException e) { 318 | LogUtil.e(TAG, e); 319 | } finally { 320 | closeIO(inputStream, outputStream); 321 | } 322 | return false; 323 | } 324 | 325 | /** 326 | * 关流操作 327 | * 328 | * @param closeables 329 | */ 330 | public static void closeIO(Closeable... closeables) { 331 | if (closeables != null && closeables.length > 0) { 332 | Closeable[] var4 = closeables; 333 | int var3 = closeables.length; 334 | 335 | for (int var2 = 0; var2 < var3; ++var2) { 336 | Closeable cb = var4[var2]; 337 | 338 | try { 339 | if (cb != null) { 340 | cb.close(); 341 | } 342 | } catch (IOException var6) { 343 | LogUtil.e(TAG, var6); 344 | } 345 | } 346 | 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/ShareReflectUtil.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.content.Context; 4 | 5 | import java.lang.reflect.Array; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.Method; 9 | import java.util.Arrays; 10 | 11 | // 获取路径的类 12 | public class ShareReflectUtil { 13 | 14 | /** 15 | * Locates a given field anywhere in the class inheritance hierarchy. 16 | * 17 | * @param instance an object to search the field into. 18 | * @param name field name 19 | * @return a field object 20 | * @throws NoSuchFieldException if the field cannot be located 21 | */ 22 | public static Field findField(Object instance, String name) throws NoSuchFieldException { 23 | for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { 24 | try { 25 | Field field = clazz.getDeclaredField(name); 26 | 27 | if (!field.isAccessible()) { 28 | field.setAccessible(true); 29 | } 30 | 31 | return field; 32 | } catch (NoSuchFieldException e) { 33 | // ignore and search next 34 | } 35 | } 36 | 37 | throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass()); 38 | } 39 | 40 | public static Field findField(Class originClazz, String name) throws NoSuchFieldException { 41 | for (Class clazz = originClazz; clazz != null; clazz = clazz.getSuperclass()) { 42 | try { 43 | Field field = clazz.getDeclaredField(name); 44 | 45 | if (!field.isAccessible()) { 46 | field.setAccessible(true); 47 | } 48 | 49 | return field; 50 | } catch (NoSuchFieldException e) { 51 | // ignore and search next 52 | } 53 | } 54 | 55 | throw new NoSuchFieldException("Field " + name + " not found in " + originClazz); 56 | } 57 | 58 | /** 59 | * Locates a given method anywhere in the class inheritance hierarchy. 60 | * 61 | * @param instance an object to search the method into. 62 | * @param name method name 63 | * @param parameterTypes method parameter types 64 | * @return a method object 65 | * @throws NoSuchMethodException if the method cannot be located 66 | */ 67 | public static Method findMethod(Object instance, String name, Class... parameterTypes) 68 | throws NoSuchMethodException { 69 | for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { 70 | try { 71 | Method method = clazz.getDeclaredMethod(name, parameterTypes); 72 | 73 | if (!method.isAccessible()) { 74 | method.setAccessible(true); 75 | } 76 | 77 | return method; 78 | } catch (NoSuchMethodException e) { 79 | // ignore and search next 80 | } 81 | } 82 | 83 | throw new NoSuchMethodException("Method " 84 | + name 85 | + " with parameters " 86 | + Arrays.asList(parameterTypes) 87 | + " not found in " + instance.getClass()); 88 | } 89 | 90 | /** 91 | * Locates a given method anywhere in the class inheritance hierarchy. 92 | * 93 | * @param clazz a class to search the method into. 94 | * @param name method name 95 | * @param parameterTypes method parameter types 96 | * @return a method object 97 | * @throws NoSuchMethodException if the method cannot be located 98 | */ 99 | public static Method findMethod(Class clazz, String name, Class... parameterTypes) 100 | throws NoSuchMethodException { 101 | for (; clazz != null; clazz = clazz.getSuperclass()) { 102 | try { 103 | Method method = clazz.getDeclaredMethod(name, parameterTypes); 104 | 105 | if (!method.isAccessible()) { 106 | method.setAccessible(true); 107 | } 108 | 109 | return method; 110 | } catch (NoSuchMethodException e) { 111 | // ignore and search next 112 | } 113 | } 114 | 115 | throw new NoSuchMethodException("Method " 116 | + name 117 | + " with parameters " 118 | + Arrays.asList(parameterTypes) 119 | + " not found in " + clazz); 120 | } 121 | 122 | /** 123 | * Locates a given constructor anywhere in the class inheritance hierarchy. 124 | * 125 | * @param instance an object to search the constructor into. 126 | * @param parameterTypes constructor parameter types 127 | * @return a constructor object 128 | * @throws NoSuchMethodException if the constructor cannot be located 129 | */ 130 | public static Constructor findConstructor(Object instance, Class... parameterTypes) 131 | throws NoSuchMethodException { 132 | for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { 133 | try { 134 | Constructor ctor = clazz.getDeclaredConstructor(parameterTypes); 135 | 136 | if (!ctor.isAccessible()) { 137 | ctor.setAccessible(true); 138 | } 139 | 140 | return ctor; 141 | } catch (NoSuchMethodException e) { 142 | // ignore and search next 143 | } 144 | } 145 | 146 | throw new NoSuchMethodException("Constructor" 147 | + " with parameters " 148 | + Arrays.asList(parameterTypes) 149 | + " not found in " + instance.getClass()); 150 | } 151 | 152 | /** 153 | * Replace the value of a field containing a non null array, by a new array containing the 154 | * elements of the original array plus the elements of extraElements. 155 | * 156 | * @param instance the instance whose field is to be modified. 157 | * @param fieldName the field to modify. 158 | * @param extraElements elements to append at the end of the array. 159 | */ 160 | public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) 161 | throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { 162 | Field jlrField = findField(instance, fieldName); 163 | 164 | Object[] original = (Object[]) jlrField.get(instance); 165 | Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length); 166 | 167 | // NOTE: changed to copy extraElements first, for patch load first 168 | 169 | System.arraycopy(extraElements, 0, combined, 0, extraElements.length); 170 | System.arraycopy(original, 0, combined, extraElements.length, original.length); 171 | 172 | jlrField.set(instance, combined); 173 | } 174 | 175 | /** 176 | * Replace the value of a field containing a non null array, by a new array containing the 177 | * elements of the original array plus the elements of extraElements. 178 | * 179 | * @param instance the instance whose field is to be modified. 180 | * @param fieldName the field to modify. 181 | */ 182 | public static void reduceFieldArray(Object instance, String fieldName, int reduceSize) 183 | throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { 184 | if (reduceSize <= 0) { 185 | return; 186 | } 187 | 188 | Field jlrField = findField(instance, fieldName); 189 | 190 | Object[] original = (Object[]) jlrField.get(instance); 191 | int finalLength = original.length - reduceSize; 192 | 193 | if (finalLength <= 0) { 194 | return; 195 | } 196 | 197 | Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), finalLength); 198 | 199 | System.arraycopy(original, reduceSize, combined, 0, finalLength); 200 | 201 | jlrField.set(instance, combined); 202 | } 203 | 204 | public static Object getActivityThread(Context context, 205 | Class activityThread) { 206 | try { 207 | if (activityThread == null) { 208 | activityThread = Class.forName("android.app.ActivityThread"); 209 | } 210 | Method m = activityThread.getMethod("currentActivityThread"); 211 | m.setAccessible(true); 212 | Object currentActivityThread = m.invoke(null); 213 | if (currentActivityThread == null && context != null) { 214 | // In older versions of Android (prior to frameworks/base 66a017b63461a22842) 215 | // the currentActivityThread was built on thread locals, so we'll need to try 216 | // even harder 217 | Field mLoadedApk = context.getClass().getField("mLoadedApk"); 218 | mLoadedApk.setAccessible(true); 219 | Object apk = mLoadedApk.get(context); 220 | Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread"); 221 | mActivityThreadField.setAccessible(true); 222 | currentActivityThread = mActivityThreadField.get(apk); 223 | } 224 | return currentActivityThread; 225 | } catch (Throwable ignore) { 226 | return null; 227 | } 228 | } 229 | 230 | /** 231 | * Handy method for fetching hidden integer constant value in system classes. 232 | * 233 | * @param clazz 234 | * @param fieldName 235 | * @return 236 | */ 237 | public static int getValueOfStaticIntField(Class clazz, String fieldName, int defVal) { 238 | try { 239 | final Field field = findField(clazz, fieldName); 240 | return field.getInt(null); 241 | } catch (Throwable thr) { 242 | return defVal; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/SharedTool.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | 7 | import java.util.Iterator; 8 | import java.util.Map; 9 | 10 | /** 11 | * 使用SharedPreferences通用数据存储接口 12 | *

13 | * Created by hzj on 2015/10/7. 14 | */ 15 | public class SharedTool { 16 | 17 | private static SharedPreferences sp; 18 | private static SharedTool sharedTool; 19 | 20 | private SharedTool(Context context) { 21 | Context mContext = context.getApplicationContext(); 22 | if(mContext == null){ 23 | mContext = context; 24 | } 25 | sp = mContext.getSharedPreferences("com.tc.bubblelayout", Activity.MODE_PRIVATE); 26 | } 27 | 28 | public static SharedTool getInstance(Context context) { 29 | if (sharedTool == null) { 30 | synchronized (SharedTool.class) { 31 | if (sharedTool == null) { 32 | sharedTool = new SharedTool(context); 33 | } 34 | } 35 | } 36 | return sharedTool; 37 | } 38 | 39 | private SharedPreferences.Editor getEditor() { 40 | return sp.edit(); 41 | } 42 | 43 | public boolean saveInt(String key, int value) { 44 | return getEditor().putInt(key, value).commit(); 45 | } 46 | 47 | public int getInt(String key) { 48 | return sp.getInt(key, 0); 49 | } 50 | 51 | public int getInt(String key, int defValue) { 52 | return sp.getInt(key, defValue); 53 | } 54 | 55 | public boolean saveLong(String key, long value) { 56 | return getEditor().putLong(key, value).commit(); 57 | } 58 | 59 | public long getLong(String key) { 60 | return sp.getLong(key, 0); 61 | } 62 | 63 | public long getLong(String key, long defautValue) { 64 | return sp.getLong(key, defautValue); 65 | } 66 | 67 | public boolean saveString(String key, String value) { 68 | return getEditor().putString(key, value).commit(); 69 | } 70 | 71 | public String getString(String key) { 72 | return sp.getString(key, ""); 73 | } 74 | 75 | public boolean saveBoolean(String key, boolean value) { 76 | return getEditor().putBoolean(key, value).commit(); 77 | } 78 | 79 | public boolean getBoolean(String key) { 80 | return sp.getBoolean(key, false); 81 | } 82 | 83 | public boolean getBoolean(String key, boolean def) { 84 | return sp.getBoolean(key, def); 85 | } 86 | 87 | public boolean remove(String key) { 88 | return sp.edit().remove(key).commit(); 89 | } 90 | 91 | public boolean contains(String key) { 92 | return sp.contains(key); 93 | } 94 | 95 | public void saveStringList(Map stringMap){ 96 | if (stringMap == null || stringMap.size() == 0){ 97 | return; 98 | } 99 | Iterator> entries = stringMap.entrySet().iterator(); 100 | while (entries.hasNext()){ 101 | Map.Entry entry = entries.next(); 102 | sp.edit().putString(entry.getKey(),entry.getValue()); 103 | } 104 | sp.edit().commit(); 105 | } 106 | } -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/TinkerLoadLibrary.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.Method; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class TinkerLoadLibrary { 14 | private static final String TAG = "TinkerLoadLibrary"; 15 | 16 | public static synchronized void installNativeLibraryPath(ClassLoader classLoader, File folder) 17 | throws Throwable { 18 | if (folder == null || !folder.exists()) { 19 | LogUtil.e(TAG, "installNativeLibraryPath, folder %s is illegal" + folder); 20 | return; 21 | } 22 | // android o sdk_int 26 23 | // for android o preview sdk_int 25 24 | if ((Build.VERSION.SDK_INT == 25 && getPreviousSdkInt() != 0) 25 | || Build.VERSION.SDK_INT > 25) { 26 | try { 27 | V25.install(classLoader, folder); 28 | return; 29 | } catch (Throwable throwable) { 30 | // install fail, try to treat it as v23 31 | // some preview N version may go here 32 | LogUtil.e(TAG, "installNativeLibraryPath, v25 fail, sdk: %d, error: %s, try to fallback to V23", 33 | throwable); 34 | V23.install(classLoader, folder); 35 | } 36 | } else if (Build.VERSION.SDK_INT >= 23) { 37 | try { 38 | V23.install(classLoader, folder); 39 | } catch (Throwable throwable) { 40 | // install fail, try to treat it as v14 41 | LogUtil.e(TAG, "installNativeLibraryPath, v23 fail, sdk: %d, error: %s, try to fallback to V14", 42 | throwable); 43 | 44 | V14.install(classLoader, folder); 45 | } 46 | } else if (Build.VERSION.SDK_INT >= 14) { 47 | V14.install(classLoader, folder); 48 | } 49 | } 50 | 51 | /** 52 | * fuck部分机型删了该成员属性,兼容 53 | * 54 | * @return 被厂家删了返回1,否则正常读取 55 | */ 56 | @TargetApi(Build.VERSION_CODES.M) 57 | private static int getPreviousSdkInt() { 58 | try { 59 | return Build.VERSION.PREVIEW_SDK_INT; 60 | } catch (Throwable ignore) { 61 | } 62 | return 1; 63 | } 64 | 65 | private static final class V14 { 66 | private static void install(ClassLoader classLoader, File folder) throws Throwable { 67 | Field pathListField = ShareReflectUtil.findField(classLoader, "pathList"); 68 | Object dexPathList = pathListField.get(classLoader); 69 | 70 | ShareReflectUtil.expandFieldArray(dexPathList, "nativeLibraryDirectories", new File[]{folder}); 71 | } 72 | } 73 | 74 | private static final class V23 { 75 | private static void install(ClassLoader classLoader, File folder) throws Throwable { 76 | Field pathListField = ShareReflectUtil.findField(classLoader, "pathList"); 77 | Object dexPathList = pathListField.get(classLoader); 78 | 79 | Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories"); 80 | 81 | List libDirs = (List) nativeLibraryDirectories.get(dexPathList); 82 | libDirs.add(0, folder); 83 | Field systemNativeLibraryDirectories = 84 | ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories"); 85 | List systemLibDirs = (List) systemNativeLibraryDirectories.get(dexPathList); 86 | Method makePathElements = 87 | ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class); 88 | ArrayList suppressedExceptions = new ArrayList<>(); 89 | libDirs.addAll(systemLibDirs); 90 | Object[] elements = (Object[]) makePathElements. 91 | invoke(dexPathList, libDirs, null, suppressedExceptions); 92 | Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements"); 93 | nativeLibraryPathElements.setAccessible(true); 94 | nativeLibraryPathElements.set(dexPathList, elements); 95 | LogUtil.i(TAG, "libDirs:" + libDirs); 96 | } 97 | } 98 | 99 | private static final class V25 { 100 | private static void install(ClassLoader classLoader, File folder) throws Throwable { 101 | Field pathListField = ShareReflectUtil.findField(classLoader, "pathList"); 102 | Object dexPathList = pathListField.get(classLoader); 103 | 104 | Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories"); 105 | 106 | List libDirs = (List) nativeLibraryDirectories.get(dexPathList); 107 | libDirs.add(0, folder); 108 | Field systemNativeLibraryDirectories = 109 | ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories"); 110 | List systemLibDirs = (List) systemNativeLibraryDirectories.get(dexPathList); 111 | Method makePathElements = 112 | ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class); 113 | libDirs.addAll(systemLibDirs); 114 | Object[] elements = (Object[]) makePathElements. 115 | invoke(dexPathList, libDirs); 116 | Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements"); 117 | nativeLibraryPathElements.setAccessible(true); 118 | nativeLibraryPathElements.set(dexPathList, elements); 119 | LogUtil.i(TAG, "libDirs:" + libDirs); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/URegex.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.text.NumberFormat; 6 | import java.util.Currency; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | /** 11 | * date: 2017/12/13 16:04 12 | * description 数值类型转换util 13 | * modify by 14 | * 15 | * @author tc 16 | */ 17 | public class URegex { 18 | 19 | public static final Pattern RegImage = Pattern.compile("\\.(png|gif|jpg|jpeg|bmp)"); 20 | public static final Pattern RegUrlName = Pattern.compile("/((?!/).)*$"); 21 | public static final Pattern RegUrlDomain = Pattern.compile("^https?://[^/]*"); 22 | private static Pattern NUMERIC_PATTERN = Pattern.compile("^([-\\+]?[0-9]([0-9]*)(\\.[0-9]+)?)|(^0$)$"); 23 | private static Pattern INTEGER_OR_LONG_PATTERN = Pattern.compile("^[-\\+]?[\\d]*$"); 24 | private static Pattern DOUBLE_OR_FLOAT_PATTERN = Pattern.compile("^[-\\+]?[.\\d]*$"); 25 | 26 | /** 27 | * 判断是否为数字。包含0123,123,1.11,0.11,0123.11等 28 | * 29 | * @param value 字符串 30 | * @return 是则返回true, 否则false 31 | */ 32 | public static boolean isNumeric(String value) { 33 | if (TextUtils.isEmpty(value)) { 34 | return false; 35 | } 36 | 37 | Matcher matcher = NUMERIC_PATTERN.matcher(value); 38 | return matcher.find(); 39 | } 40 | 41 | /** 42 | * 判断是否为int或long,整数--包括0123写法。 43 | * 44 | * @param value 字符串 45 | * @return 是则返回true, 否则false 46 | */ 47 | public static boolean isIntegerOrLong(String value) { 48 | 49 | return INTEGER_OR_LONG_PATTERN.matcher(value).matches(); 50 | } 51 | 52 | 53 | /** 54 | * 判断是否为浮点数,包括double和float 55 | * 56 | * @param value 传入的字符串 57 | * @return 是浮点数返回true, 否则返回false 58 | */ 59 | public static boolean isDoubleOrFloat(String value) { 60 | return DOUBLE_OR_FLOAT_PATTERN.matcher(value).matches(); 61 | } 62 | 63 | /** 64 | * 数字字符转换为整形 65 | * 66 | * @param value 字符串 67 | * @return 返回数字,不为数字则返回0 68 | */ 69 | public static int convertInt(String value) { 70 | return convertInt(value, 0); 71 | } 72 | 73 | /** 74 | * 数字字符转换为整形 75 | * 76 | * @param value 字符串 77 | * @param defaultValue 默认值 78 | * @return 返回数字,不为数字则返回默认值 79 | */ 80 | public static int convertInt(String value, int defaultValue) { 81 | try { 82 | return Integer.parseInt(value); 83 | } catch (Exception e) { 84 | return defaultValue; 85 | } 86 | } 87 | 88 | /** 89 | * 数字字符转换为长整型 90 | * 91 | * @param value 要转换的string 92 | * @return 返回long,不为数字则返回0 93 | */ 94 | public static long convertLong(String value) { 95 | return convertLong(value, 0); 96 | } 97 | 98 | /** 99 | * 数字字符转换为长整型 100 | * 101 | * @param value 要转换的string 102 | * @param defaultValue 默认值 103 | * @return 返回long,不为数字则返回默认值 104 | */ 105 | public static long convertLong(String value, long defaultValue) { 106 | try { 107 | return Long.parseLong(value); 108 | } catch (Exception e) { 109 | return defaultValue; 110 | } 111 | } 112 | 113 | /** 114 | * 数字字符转换为双精度型 115 | * 116 | * @param value 117 | * @return 返回double,不为数字则返回0 118 | */ 119 | public static double convertDouble(String value) { 120 | return convertDouble(value, 0); 121 | } 122 | 123 | /** 124 | * 数字字符转换为双精度型 125 | * 126 | * @param value 要转换的string 127 | * @param defaultValue 默认值 128 | * @return 返回double,不为数字则返回默认值 129 | */ 130 | public static double convertDouble(String value, double defaultValue) { 131 | try { 132 | return Double.parseDouble(value); 133 | } catch (Exception e) { 134 | return defaultValue; 135 | } 136 | } 137 | 138 | 139 | /** 140 | * 数字字符转换单双精度型 141 | * 142 | * @param value 143 | * @return float,不为数字则返回0 144 | */ 145 | public static float convertFloat(String value) { 146 | return convertFloat(value, 0); 147 | } 148 | 149 | /** 150 | * 数字字符转换为单精度型 151 | * 152 | * @param value 153 | * @param defaultValue 默认值 154 | * @return float,不为数字则返回默认值 155 | */ 156 | public static float convertFloat(String value, float defaultValue) { 157 | try { 158 | return Float.parseFloat(value); 159 | } catch (Exception e) { 160 | return defaultValue; 161 | } 162 | } 163 | 164 | /** 165 | * 转换为boolean 166 | * 167 | * @param value value 168 | * @param defaultValue defaultValue 169 | * @return 170 | */ 171 | public static boolean toBoolean(String value, boolean defaultValue) { 172 | try { 173 | return Boolean.parseBoolean(value); 174 | } catch (NumberFormatException e) { 175 | return defaultValue; 176 | } 177 | } 178 | 179 | /** 180 | * 转换为boolean类型 181 | * 182 | * @param value 183 | * @return 184 | */ 185 | public static boolean toBoolean(String value) { 186 | return toBoolean(value, false); 187 | } 188 | 189 | /** 190 | * 本方法主要是为了防止,和.出现显示错误。默认格式化为US。获取当前机器的默认语言来格式化对应的货币 191 | * 192 | * @param price 要格式化的价格 193 | * @param symbol 当前的货币符号 194 | * @return 当前货币符号对应的价格展示string 195 | */ 196 | public static String getPriceStrByLocale(double price, String symbol) { 197 | NumberFormat format = NumberFormat.getNumberInstance(); 198 | if (symbol.trim().equals("JP¥")) { 199 | int result = RoundUtil.roundReturnInt(price, 0); 200 | return format.format(result); 201 | 202 | } 203 | return format.format(price); 204 | } 205 | 206 | /** 207 | * 本方法主要是为了防止,和.出现显示错误。默认格式化为US。获取当前机器的默认语言来格式化对应的货币 208 | * 209 | * @param price 要格式化的价格 210 | * @return 当前货币符号对应的价格展示string 211 | */ 212 | public static String getPriceStrByLocale(double price) { 213 | NumberFormat currencyInstance = NumberFormat.getCurrencyInstance(); 214 | Currency currency = currencyInstance.getCurrency(); 215 | return getPriceStrByLocale(price, currency.getSymbol()); 216 | } 217 | 218 | /** 219 | * 过滤url 220 | * 221 | * @param url url 222 | * @return 过滤后的url 223 | */ 224 | public static String getDomain(String url) { 225 | if (url == null) { 226 | return ""; 227 | } 228 | Matcher matcher = RegUrlDomain.matcher(url); 229 | String value = ""; 230 | if (matcher.find()) { 231 | value = matcher.group(); 232 | } 233 | //ULog.db(value); 234 | return value; 235 | } 236 | 237 | /** 238 | * 根据相应的正式表达则,查看是否匹配 239 | * 240 | * @param content 内容 241 | * @param reg 正则表达式 242 | * @return 过滤后的内容,没有则返回空字符串 243 | */ 244 | public static String match(String content, String reg) { 245 | if (content == null) { 246 | return ""; 247 | } 248 | Pattern pattern = Pattern.compile(reg); 249 | Matcher matcher = pattern.matcher(content); 250 | String value = ""; 251 | if (matcher.find()) { 252 | value = matcher.group(); 253 | } 254 | return value; 255 | } 256 | 257 | /** 258 | * 根据相应的正式表达则,查看是否匹配 259 | * 260 | * @param content 内容 261 | * @param pattern 正则表达式 262 | * @return 过滤后的内容,没有则返回空字符串 263 | */ 264 | public static String match(String content, Pattern pattern) { 265 | if (content == null) { 266 | return ""; 267 | } 268 | Matcher matcher = pattern.matcher(content); 269 | String value = ""; 270 | if (matcher.find()) { 271 | value = matcher.group(); 272 | } 273 | return value; 274 | } 275 | 276 | /** 277 | * 校验邮箱格式 278 | * 279 | * @param email 邮箱字符串 280 | * @return 正确格式返回true, 否则false 281 | */ 282 | public static boolean matchEmail(String email) { 283 | String check = "^[a-zA-Z\\d]+([-._][a-zA-Z\\d]+)*@[a-zA-Z\\d]+((-[a-zA-Z\\d]+)?)+([\\" + 284 | ".][a-zA-Z]+)+$"; 285 | Pattern regex = Pattern.compile(check); 286 | Matcher matcher = regex.matcher(email); 287 | return matcher.matches(); 288 | } 289 | 290 | /** 291 | * 是否匹配字符串 292 | * 293 | * @param content 内容 294 | * @param reg 正则式 295 | * @return 匹配则返回true,否则false 296 | */ 297 | public static boolean isMatch(String content, String reg) { 298 | Pattern pattern = Pattern.compile(reg); 299 | Matcher matcher = pattern.matcher(content); 300 | return matcher.find(); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/blur/BitmapBlurHelper.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.blur; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.os.Build; 8 | import android.renderscript.RSRuntimeException; 9 | 10 | /** 11 | * 对Bitmap进行高斯模糊处理 12 | *

13 | * Created by htliu on 16/11/15. 14 | */ 15 | public final class BitmapBlurHelper { 16 | 17 | /** 18 | * 对Bitmap进行高斯模糊处理 19 | * 20 | * @param context Context 21 | * @param source Bitmap 22 | * @return Bitmap 23 | */ 24 | public static Bitmap blur(Context context, Bitmap source) { 25 | int sampling = 1; 26 | int radius = 25; 27 | 28 | int width = source.getWidth(); 29 | int height = source.getHeight(); 30 | int scaledWidth = width / sampling; 31 | int scaledHeight = height / sampling; 32 | Bitmap blurredBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); 33 | 34 | Canvas canvas = new Canvas(blurredBitmap); 35 | canvas.scale(1.0F / (float) sampling, 1.0F / (float) sampling); 36 | Paint paint = new Paint(); 37 | paint.setFlags(Paint.FILTER_BITMAP_FLAG); 38 | canvas.drawBitmap(source, 0.0F, 0.0F, paint); 39 | 40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 41 | try { 42 | blurredBitmap = RSBlur.blur(context, blurredBitmap, radius); 43 | } catch (RSRuntimeException var11) { 44 | blurredBitmap = FastBlur.blur(blurredBitmap, radius, true); 45 | } 46 | } else { 47 | blurredBitmap = FastBlur.blur(blurredBitmap, radius, true); 48 | } 49 | 50 | Bitmap scaledBitmap = Bitmap.createScaledBitmap(blurredBitmap, source.getWidth(), source.getHeight(), true); 51 | blurredBitmap.recycle(); 52 | return scaledBitmap; 53 | } 54 | 55 | /** 56 | * 做高斯模糊处理 57 | * @param source Bitmap 58 | * @param radius 值越大越模糊,取值范围1~100 59 | */ 60 | public static void blur(Bitmap source, int radius) { 61 | FastBlur.blur(source, radius, true); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/blur/FastBlur.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.blur; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | /** 6 | * Copyright (C) 2015 Wasabeef 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | public class FastBlur { 22 | 23 | public static Bitmap blur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) { 24 | 25 | // Stack Blur v1.0 from 26 | // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html 27 | // 28 | // Java Author: Mario Klingemann 29 | // http://incubator.quasimondo.com 30 | // created Feburary 29, 2004 31 | // Android port : Yahel Bouaziz 32 | // http://www.kayenko.com 33 | // ported april 5th, 2012 34 | 35 | // This is a compromise between Gaussian Blur and Box blur 36 | // It creates much better looking blurs than Box Blur, but is 37 | // 7x faster than my Gaussian Blur implementation. 38 | // 39 | // I called it Stack Blur because this describes best how this 40 | // filter works internally: it creates a kind of moving stack 41 | // of colors whilst scanning through the image. Thereby it 42 | // just has to add one new block of color to the right side 43 | // of the stack and remove the leftmost color. The remaining 44 | // colors on the topmost layer of the stack are either added on 45 | // or reduced by one, depending on if they are on the right or 46 | // on the left side of the stack. 47 | // 48 | // If you are using this algorithm in your code please add 49 | // the following line: 50 | // 51 | // Stack Blur Algorithm by Mario Klingemann 52 | 53 | Bitmap bitmap; 54 | if (canReuseInBitmap) { 55 | bitmap = sentBitmap; 56 | } else { 57 | bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); 58 | } 59 | 60 | if (radius < 1) { 61 | return (null); 62 | } 63 | 64 | int w = bitmap.getWidth(); 65 | int h = bitmap.getHeight(); 66 | 67 | int[] pix = new int[w * h]; 68 | bitmap.getPixels(pix, 0, w, 0, 0, w, h); 69 | 70 | int wm = w - 1; 71 | int hm = h - 1; 72 | int wh = w * h; 73 | int div = radius + radius + 1; 74 | 75 | int r[] = new int[wh]; 76 | int g[] = new int[wh]; 77 | int b[] = new int[wh]; 78 | int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; 79 | int vmin[] = new int[Math.max(w, h)]; 80 | 81 | int divsum = (div + 1) >> 1; 82 | divsum *= divsum; 83 | int dv[] = new int[256 * divsum]; 84 | for (i = 0; i < 256 * divsum; i++) { 85 | dv[i] = (i / divsum); 86 | } 87 | 88 | yw = yi = 0; 89 | 90 | int[][] stack = new int[div][3]; 91 | int stackpointer; 92 | int stackstart; 93 | int[] sir; 94 | int rbs; 95 | int r1 = radius + 1; 96 | int routsum, goutsum, boutsum; 97 | int rinsum, ginsum, binsum; 98 | 99 | for (y = 0; y < h; y++) { 100 | rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; 101 | for (i = -radius; i <= radius; i++) { 102 | p = pix[yi + Math.min(wm, Math.max(i, 0))]; 103 | sir = stack[i + radius]; 104 | sir[0] = (p & 0xff0000) >> 16; 105 | sir[1] = (p & 0x00ff00) >> 8; 106 | sir[2] = (p & 0x0000ff); 107 | rbs = r1 - Math.abs(i); 108 | rsum += sir[0] * rbs; 109 | gsum += sir[1] * rbs; 110 | bsum += sir[2] * rbs; 111 | if (i > 0) { 112 | rinsum += sir[0]; 113 | ginsum += sir[1]; 114 | binsum += sir[2]; 115 | } else { 116 | routsum += sir[0]; 117 | goutsum += sir[1]; 118 | boutsum += sir[2]; 119 | } 120 | } 121 | stackpointer = radius; 122 | 123 | for (x = 0; x < w; x++) { 124 | 125 | r[yi] = dv[rsum]; 126 | g[yi] = dv[gsum]; 127 | b[yi] = dv[bsum]; 128 | 129 | rsum -= routsum; 130 | gsum -= goutsum; 131 | bsum -= boutsum; 132 | 133 | stackstart = stackpointer - radius + div; 134 | sir = stack[stackstart % div]; 135 | 136 | routsum -= sir[0]; 137 | goutsum -= sir[1]; 138 | boutsum -= sir[2]; 139 | 140 | if (y == 0) { 141 | vmin[x] = Math.min(x + radius + 1, wm); 142 | } 143 | p = pix[yw + vmin[x]]; 144 | 145 | sir[0] = (p & 0xff0000) >> 16; 146 | sir[1] = (p & 0x00ff00) >> 8; 147 | sir[2] = (p & 0x0000ff); 148 | 149 | rinsum += sir[0]; 150 | ginsum += sir[1]; 151 | binsum += sir[2]; 152 | 153 | rsum += rinsum; 154 | gsum += ginsum; 155 | bsum += binsum; 156 | 157 | stackpointer = (stackpointer + 1) % div; 158 | sir = stack[(stackpointer) % div]; 159 | 160 | routsum += sir[0]; 161 | goutsum += sir[1]; 162 | boutsum += sir[2]; 163 | 164 | rinsum -= sir[0]; 165 | ginsum -= sir[1]; 166 | binsum -= sir[2]; 167 | 168 | yi++; 169 | } 170 | yw += w; 171 | } 172 | for (x = 0; x < w; x++) { 173 | rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; 174 | yp = -radius * w; 175 | for (i = -radius; i <= radius; i++) { 176 | yi = Math.max(0, yp) + x; 177 | 178 | sir = stack[i + radius]; 179 | 180 | sir[0] = r[yi]; 181 | sir[1] = g[yi]; 182 | sir[2] = b[yi]; 183 | 184 | rbs = r1 - Math.abs(i); 185 | 186 | rsum += r[yi] * rbs; 187 | gsum += g[yi] * rbs; 188 | bsum += b[yi] * rbs; 189 | 190 | if (i > 0) { 191 | rinsum += sir[0]; 192 | ginsum += sir[1]; 193 | binsum += sir[2]; 194 | } else { 195 | routsum += sir[0]; 196 | goutsum += sir[1]; 197 | boutsum += sir[2]; 198 | } 199 | 200 | if (i < hm) { 201 | yp += w; 202 | } 203 | } 204 | yi = x; 205 | stackpointer = radius; 206 | for (y = 0; y < h; y++) { 207 | // Preserve alpha channel: ( 0xff000000 & pix[yi] ) 208 | pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; 209 | 210 | rsum -= routsum; 211 | gsum -= goutsum; 212 | bsum -= boutsum; 213 | 214 | stackstart = stackpointer - radius + div; 215 | sir = stack[stackstart % div]; 216 | 217 | routsum -= sir[0]; 218 | goutsum -= sir[1]; 219 | boutsum -= sir[2]; 220 | 221 | if (x == 0) { 222 | vmin[y] = Math.min(y + r1, hm) * w; 223 | } 224 | p = x + vmin[y]; 225 | 226 | sir[0] = r[p]; 227 | sir[1] = g[p]; 228 | sir[2] = b[p]; 229 | 230 | rinsum += sir[0]; 231 | ginsum += sir[1]; 232 | binsum += sir[2]; 233 | 234 | rsum += rinsum; 235 | gsum += ginsum; 236 | bsum += binsum; 237 | 238 | stackpointer = (stackpointer + 1) % div; 239 | sir = stack[stackpointer]; 240 | 241 | routsum += sir[0]; 242 | goutsum += sir[1]; 243 | boutsum += sir[2]; 244 | 245 | rinsum -= sir[0]; 246 | ginsum -= sir[1]; 247 | binsum -= sir[2]; 248 | 249 | yi += w; 250 | } 251 | } 252 | 253 | bitmap.setPixels(pix, 0, w, 0, 0, w, h); 254 | 255 | return (bitmap); 256 | } 257 | } -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/blur/RSBlur.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.blur; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.os.Build; 7 | import android.renderscript.Allocation; 8 | import android.renderscript.Element; 9 | import android.renderscript.RSRuntimeException; 10 | import android.renderscript.RenderScript; 11 | import android.renderscript.ScriptIntrinsicBlur; 12 | 13 | /** 14 | * Copyright (C) 2015 Wasabeef 15 | * 16 | * Licensed under the Apache License, Version 2.0 (the "License"); 17 | * you may not use this file except in compliance with the License. 18 | * You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, software 23 | * distributed under the License is distributed on an "AS IS" BASIS, 24 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | * See the License for the specific language governing permissions and 26 | * limitations under the License. 27 | */ 28 | 29 | public class RSBlur { 30 | 31 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 32 | public static Bitmap blur(Context context, Bitmap bitmap, int radius) throws RSRuntimeException { 33 | RenderScript rs = null; 34 | try { 35 | rs = RenderScript.create(context); 36 | Allocation input = 37 | Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE, 38 | Allocation.USAGE_SCRIPT); 39 | Allocation output = Allocation.createTyped(rs, input.getType()); 40 | ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); 41 | 42 | blur.setInput(input); 43 | blur.setRadius(radius); 44 | blur.forEach(output); 45 | output.copyTo(bitmap); 46 | } finally { 47 | if (rs != null) { 48 | rs.destroy(); 49 | } 50 | } 51 | 52 | return bitmap; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/config/ImagePipelineConfigFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | package com.tc.bubblelayout.fresco.config; 14 | 15 | 16 | import android.content.Context; 17 | import android.os.Build; 18 | import android.os.Environment; 19 | 20 | import com.facebook.cache.disk.DiskCacheConfig; 21 | import com.facebook.common.internal.Supplier; 22 | import com.facebook.common.memory.MemoryTrimType; 23 | import com.facebook.common.memory.MemoryTrimmable; 24 | import com.facebook.common.memory.NoOpMemoryTrimmableRegistry; 25 | import com.facebook.common.util.ByteConstants; 26 | import com.facebook.imagepipeline.cache.MemoryCacheParams; 27 | import com.facebook.imagepipeline.core.ImagePipelineConfig; 28 | import com.facebook.imagepipeline.core.ImagePipelineFactory; 29 | import com.facebook.imagepipeline.decoder.SimpleProgressiveJpegConfig; 30 | import com.tc.bubblelayout.LogUtil; 31 | 32 | import java.io.File; 33 | import java.util.Locale; 34 | 35 | /** 36 | * Creates ImagePipeline configuration for the sample app 37 | */ 38 | public class ImagePipelineConfigFactory { 39 | private static final String TAG = "ImagePipelineConfigFactory"; 40 | private static final int MAX_MEMORY_CACHE_SIZE = 80 * ByteConstants.MB; 41 | private static final int MAX_DISK_CACHE_SIZE = 500 * ByteConstants.MB; 42 | private static final int MAX_DISK_CACHE_SIZE_ON_LOW_DISK_SPACE = 60 * ByteConstants.MB; 43 | private static final int MAX_DISK_CACHE_SIZE_ON_VERY_LOW_DISK_SPACE = 30 * ByteConstants.MB; 44 | 45 | private static int mMaxMemoryCacheSize = MAX_MEMORY_CACHE_SIZE; 46 | private static int mMaxDiskCacheSize = MAX_MEMORY_CACHE_SIZE; 47 | 48 | /** 49 | * 可以设置最大的内存缓存大小 50 | * 51 | * @param memoryCacheSize 内存缓存大小,单位MB 52 | */ 53 | public static void setMaxMemoryCacheSize(int memoryCacheSize) { 54 | mMaxMemoryCacheSize = memoryCacheSize * ByteConstants.MB; 55 | } 56 | 57 | /** 58 | * 可以设置最大的磁盘缓存大小 59 | * 60 | * @param diskCacheSize 磁盘缓存大小,单位MB 61 | */ 62 | public static void setMaxDiskCacheSize(int diskCacheSize) { 63 | mMaxDiskCacheSize = diskCacheSize * ByteConstants.MB; 64 | } 65 | 66 | /** 67 | * 图片配置 68 | * 69 | * @param context 上下文 70 | * @return ImagePipelineConfig 71 | */ 72 | public static ImagePipelineConfig getImagePipelineConfig(Context context) { 73 | 74 | if (mMaxDiskCacheSize <= 0) { 75 | mMaxDiskCacheSize = MAX_DISK_CACHE_SIZE; 76 | } 77 | if (mMaxMemoryCacheSize <= 0) { 78 | mMaxMemoryCacheSize = MAX_MEMORY_CACHE_SIZE; 79 | } 80 | 81 | //图片配置 82 | ImagePipelineConfig.Builder imagePipelineConfigBuilder = ImagePipelineConfig.newBuilder 83 | (context); 84 | 85 | imagePipelineConfigBuilder 86 | .setBitmapMemoryCacheParamsSupplier( 87 | new Supplier() { 88 | @Override 89 | public MemoryCacheParams get() { 90 | int currentCount = (int) Runtime.getRuntime().maxMemory(); 91 | int currentMaxMemory = currentCount / 5; 92 | if (currentMaxMemory > mMaxMemoryCacheSize) { 93 | LogUtil.w(TAG, "当前图片内存分配总大小,分配过大,减少内存缓存总大小,mMaxMemoryCacheSize:" + 94 | mMaxMemoryCacheSize); 95 | currentMaxMemory = mMaxMemoryCacheSize; 96 | } 97 | 98 | LogUtil.e(TAG, "当前图片内存分配总大小:" + String.valueOf(currentMaxMemory)); 99 | MemoryCacheParams bitmapCacheParams = new MemoryCacheParams( 100 | // Max cache entry size 101 | currentMaxMemory, 102 | // Max total size of elements in the cache 103 | Integer.MAX_VALUE, 104 | // Max entries in the cache 105 | currentMaxMemory, 106 | // Max total size of elements in eviction queue 107 | Integer.MAX_VALUE, 108 | // Max length of eviction queue 109 | Integer.MAX_VALUE); 110 | return bitmapCacheParams; 111 | } 112 | }) 113 | 114 | //磁盘缓存配置 115 | .setMainDiskCacheConfig(DiskCacheConfig.newBuilder(context) 116 | .setBaseDirectoryPath(getExternalCacheDir(context)) 117 | .setBaseDirectoryName(context.getPackageName()) 118 | //默认缓存的最大大小。 119 | .setMaxCacheSize(MAX_DISK_CACHE_SIZE) 120 | // 缓存的最大大小,使用设备时低磁盘空间。 121 | .setMaxCacheSizeOnLowDiskSpace(MAX_DISK_CACHE_SIZE_ON_LOW_DISK_SPACE) 122 | // 缓存的最大大小,当设备极低磁盘空间 123 | .setMaxCacheSizeOnVeryLowDiskSpace(MAX_DISK_CACHE_SIZE_ON_VERY_LOW_DISK_SPACE) 124 | .build()); 125 | 126 | NoOpMemoryTrimmableRegistry.getInstance().registerMemoryTrimmable(new MemoryTrimmable() { 127 | @Override 128 | public void trim(MemoryTrimType trimType) { 129 | final double suggestedTrimRatio = trimType.getSuggestedTrimRatio(); 130 | 131 | LogUtil.e(TAG, String.format(Locale.ENGLISH, "onCreate suggestedTrimRatio : %d", (int) 132 | suggestedTrimRatio)); 133 | if (MemoryTrimType.OnCloseToDalvikHeapLimit.getSuggestedTrimRatio() == 134 | suggestedTrimRatio 135 | || MemoryTrimType.OnSystemLowMemoryWhileAppInBackground 136 | .getSuggestedTrimRatio() == suggestedTrimRatio 137 | || MemoryTrimType.OnSystemLowMemoryWhileAppInForeground 138 | .getSuggestedTrimRatio() == suggestedTrimRatio 139 | ) { 140 | ImagePipelineFactory.getInstance().getImagePipeline().clearMemoryCaches(); 141 | } 142 | } 143 | }); 144 | 145 | // 146 | // imagePipelineConfigBuilder.setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier); 147 | // imagePipelineConfigBuilder.setCacheKeyFactory(cacheKeyFactory); 148 | 149 | // imagePipelineConfigBuilder.setEncodedMemoryCacheParamsSupplier 150 | // (encodedCacheParamsSupplier); 151 | //配置线程 152 | // imagePipelineConfigBuilder.setExecutorSupplier(executorSupplier); 153 | //配置统计跟踪器 154 | // imagePipelineConfigBuilder.setImageCacheStatsTracker(imageCacheStatsTracker); 155 | 156 | // 当builder.setResizeOptions(new ResizeOptions(width, height));时, 防止出现OOM, 157 | imagePipelineConfigBuilder.setDownsampleEnabled(true); 158 | // imagePipelineConfigBuilder.setMemoryTrimmableRegistry(memoryTrimmableRegistry); 159 | // imagePipelineConfigBuilder.setNetworkFetchProducer(networkFetchProducer); 160 | // imagePipelineConfigBuilder.setPoolFactory(poolFactory); 161 | imagePipelineConfigBuilder.setProgressiveJpegConfig(new SimpleProgressiveJpegConfig()); 162 | // imagePipelineConfigBuilder.setRequestListeners(requestListeners); 163 | // imagePipelineConfigBuilder.setSmallImageDiskCacheConfig(smallImageDiskCacheConfig); 164 | return imagePipelineConfigBuilder.build(); 165 | } 166 | 167 | static boolean hasExternalCacheDir() { 168 | return Build.VERSION.SDK_INT >= 8; 169 | } 170 | 171 | static File createFile(String folderPath, String fileName) { 172 | File destDir = new File(folderPath); 173 | if (!destDir.exists()) { 174 | destDir.mkdirs(); 175 | } 176 | 177 | return new File(folderPath, fileName); 178 | } 179 | 180 | static File getExternalCacheDir(Context context) { 181 | if (hasExternalCacheDir()) { 182 | return context.getExternalCacheDir(); 183 | } else { 184 | String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/"; 185 | return createFile(Environment.getExternalStorageDirectory().getPath() + cacheDir, ""); 186 | } 187 | } 188 | 189 | 190 | } 191 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/controller/SingleImageControllerListener.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.controller; 2 | 3 | import android.graphics.drawable.Animatable; 4 | import android.support.annotation.Nullable; 5 | import android.view.ViewGroup; 6 | 7 | import com.facebook.drawee.controller.BaseControllerListener; 8 | import com.facebook.drawee.view.SimpleDraweeView; 9 | import com.facebook.imagepipeline.image.ImageInfo; 10 | import com.tc.bubblelayout.fresco.utils.DensityUtil; 11 | 12 | /** 13 | * 单张图片显示控制器,当不知道图片的宽高时,可以用于重置控件的宽高 14 | * 15 | * Created by htliu on 16-11-15. 16 | */ 17 | public class SingleImageControllerListener extends BaseControllerListener { 18 | 19 | private final SimpleDraweeView draweeView; 20 | 21 | public SingleImageControllerListener(SimpleDraweeView draweeView) { 22 | this.draweeView = draweeView; 23 | } 24 | 25 | @Override 26 | public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable anim) { 27 | if (imageInfo == null || draweeView == null) { 28 | return; 29 | } 30 | 31 | ViewGroup.LayoutParams vp = draweeView.getLayoutParams(); 32 | int maxWidthSize = DensityUtil.getDisplayWidth(draweeView.getContext()); 33 | int maxHeightSize = DensityUtil.getDisplayHeight(draweeView.getContext()); 34 | int width = imageInfo.getWidth(); 35 | int height = imageInfo.getHeight(); 36 | 37 | if (width > height) { 38 | int maxWidth = DensityUtil.dipToPixels(draweeView.getContext(), maxWidthSize); 39 | if (width > maxWidth) { 40 | width = maxWidth; 41 | } 42 | vp.width = width; 43 | vp.height = (int) (imageInfo.getHeight() / (float) imageInfo.getWidth() * vp.width); 44 | } else { 45 | // width <= height 46 | int maxHeight = DensityUtil.dipToPixels(draweeView.getContext(), maxHeightSize); 47 | if (height > maxHeight) { 48 | height = maxHeight; 49 | } 50 | 51 | vp.height = height; 52 | vp.width = (int) ((float) imageInfo.getWidth() / imageInfo.getHeight() * vp.height); 53 | } 54 | 55 | draweeView.requestLayout(); 56 | } 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/listener/ILoadImageResult.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.listener; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | /** 6 | * 异步加载图片 7 | * Created by htliu on 2017/3/23. 8 | */ 9 | 10 | public interface ILoadImageResult { 11 | public void onResult(Bitmap bitmap); 12 | public void onError(String msg); 13 | } 14 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/listener/MyPostprocessor.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.listener; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import com.facebook.imagepipeline.request.BasePostprocessor; 6 | import com.tc.bubblelayout.fresco.blur.BitmapBlurHelper; 7 | 8 | /** 9 | * Created by 3020 on 2016/12/5. 10 | */ 11 | 12 | public class MyPostprocessor extends BasePostprocessor { 13 | public final static String BLUR = "blur"; 14 | public final static String RED_MESH = "red_mesh"; 15 | private String type; 16 | public MyPostprocessor(String type){ 17 | this.type = type; 18 | } 19 | 20 | @Override 21 | public void process(Bitmap bitmap) { 22 | if(type.equals(BLUR)){ 23 | BitmapBlurHelper.blur(bitmap, 35); 24 | }else if(type.equals(RED_MESH)){ 25 | int width = bitmap.getWidth(); //获取位图的宽 26 | int height = bitmap.getHeight(); //获取位图的高 27 | int []pixels = new int[width * height]; //通过位图的大小创建像素点数组 28 | bitmap.getPixels(pixels, 0, width, 0, 0, width, height); 29 | int alpha = 0xFF << 24; 30 | for(int i = 0; i < height; i++) { 31 | for(int j = 0; j < width; j++) { 32 | int grey = pixels[width * i + j]; 33 | int red = ((grey & 0x00FF0000 ) >> 16); 34 | int green = ((grey & 0x0000FF00) >> 8); 35 | int blue = (grey & 0x000000FF); 36 | grey = (int)((float) red * 0.3 + (float)green * 0.59 + (float)blue * 0.11); 37 | grey = alpha | (grey << 16) | (grey << 8) | grey; 38 | pixels[width * i + j] = grey; 39 | } 40 | } 41 | bitmap.setPixels(pixels, 0, width, 0, 0, width, height); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/utils/DensityUtil.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.utils; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.util.TypedValue; 6 | 7 | /** 8 | * Created by htliu on 16/11/15. 9 | */ 10 | public class DensityUtil { 11 | 12 | public static int getDisplayHeight(Context context) { 13 | return context.getResources().getDisplayMetrics().heightPixels; 14 | } 15 | 16 | public static int getDisplayWidth(Context context) { 17 | return context.getResources().getDisplayMetrics().widthPixels; 18 | } 19 | 20 | public static int dipToPixels(Context context, float dip) { 21 | Resources r = context.getResources(); 22 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, 23 | r.getDisplayMetrics()); 24 | return (int) px; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/fresco/utils/StreamTool.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.fresco.utils; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | /** 12 | * 功能描述:数据流处理工具类 13 | * 14 | * Created by hltiu on 16/11/15. 15 | */ 16 | public final class StreamTool { 17 | 18 | /** 19 | * 拷贝图片文件 20 | * @param oldPath 原图片所在路径 21 | * @param newPath 新图片所在路径 22 | * @throws IOException 23 | */ 24 | public static void copy(String oldPath, String newPath) throws IOException { 25 | FileInputStream inputStream = new FileInputStream(oldPath); 26 | FileOutputStream fileOutputStream = new FileOutputStream(newPath); 27 | byte[] buffer = new byte[1024]; 28 | int len; 29 | while ((len = inputStream.read(buffer)) != -1) { 30 | fileOutputStream.write(buffer, 0, len); 31 | } 32 | fileOutputStream.flush(); 33 | fileOutputStream.close(); 34 | inputStream.close(); 35 | } 36 | 37 | /** 38 | * 将byte[]写入指定的文件 39 | * @param filePath 指定文件的路径 40 | * @param data byte[] 41 | * @throws IOException 42 | */ 43 | public static void write(String filePath, byte[] data) throws IOException { 44 | FileOutputStream fileOutputStream = new FileOutputStream(filePath); 45 | fileOutputStream.write(data); 46 | fileOutputStream.flush(); 47 | fileOutputStream.close(); 48 | } 49 | 50 | /** 51 | * 根据文件路径获取byte[] 52 | * @param path 文件路径 53 | * @return 54 | * @throws IOException 55 | */ 56 | public static byte[] read(String path) throws IOException { 57 | return read(new FileInputStream(path)); 58 | } 59 | 60 | /** 61 | * 从输入流读取数据 62 | * 63 | * @param inStream 64 | * @return byte[] 65 | * @throws IOException 66 | * @throws Exception 67 | */ 68 | public static byte[] read(InputStream inStream) throws IOException { 69 | ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); 70 | byte[] buffer = new byte[1024]; 71 | int len = 0; 72 | while ((len = inStream.read(buffer)) != -1) { 73 | outSteam.write(buffer, 0, len); 74 | } 75 | outSteam.flush(); 76 | outSteam.close(); 77 | inStream.close(); 78 | return outSteam.toByteArray(); 79 | } 80 | 81 | /** 82 | * 将Bitmap对象转换成byte[] 83 | * @param bitmap Bitmap 84 | * @return byte[] 85 | * @throws IOException 86 | */ 87 | public static byte[] read(Bitmap bitmap) throws IOException { 88 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 89 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); 90 | baos.flush(); 91 | baos.close(); 92 | return baos.toByteArray(); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/GroupItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Path; 6 | import android.graphics.Rect; 7 | import android.graphics.RectF; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.View; 10 | 11 | import com.tc.bubblelayout.DensityUtil; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * author: tc 17 | * date: 2019/9/14 & 16:29 18 | * version 1.0 19 | * description 列表根据不同数据类型,拆分为多个群组进行布局显示的布局间隔绘制类 20 | * item没有触发反馈 21 | * 22 | * @see IGroupSort 23 | * modify by 24 | */ 25 | public class GroupItemDecoration extends AbstractGroupItemDecoration { 26 | 27 | private int mCornerRadius; 28 | 29 | public GroupItemDecoration(Context context, List list, int cornerRadius) { 30 | super(context, list); 31 | this.mCornerRadius = cornerRadius; 32 | } 33 | 34 | private void drawGroupCorner(Canvas canvas, View child, IGroupSort item, float[] corners) { 35 | Path srcPath = new Path(); 36 | RectF rectF = new RectF(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); 37 | if (item != null) { 38 | mPaint.setColor(mContext.getResources().getColor(item.getGroupBackgroundColorId())); 39 | } 40 | //默认无圆角 41 | srcPath.addRoundRect(rectF, corners, Path.Direction.CCW); 42 | canvas.drawPath(srcPath, mPaint); 43 | } 44 | 45 | @Override 46 | public void onDrawWhenFirstGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 47 | int adapterPosition, IGroupSort item) { 48 | float[] corners = {mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 0, 0, 0, 0}; 49 | drawGroupCorner(canvas, child, item, corners); 50 | } 51 | 52 | 53 | @Override 54 | public void onDrawWhenMiddleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 55 | int adapterPosition, IGroupSort item) { 56 | float[] corners = {0, 0, 0, 0, 0, 0, 0, 0}; 57 | drawGroupCorner(canvas, child, item, corners); 58 | } 59 | 60 | @Override 61 | public void onDrawWhenLastGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, int 62 | adapterPosition, IGroupSort item) { 63 | float[] corners = {0, 0, 0, 0, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius}; 64 | drawGroupCorner(canvas, child, item, corners); 65 | } 66 | 67 | @Override 68 | public void onDrawWhenSingleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 69 | int adapterPosition, IGroupSort item) { 70 | float[] corners = {mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 71 | mCornerRadius, mCornerRadius}; 72 | drawGroupCorner(canvas, child, item, corners); 73 | } 74 | 75 | @Override 76 | public void onDrawOverWhenFirstGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State 77 | state, int adapterPosition, IGroupSort item) { 78 | 79 | } 80 | 81 | @Override 82 | public void onDrawOverWhenMiddleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State 83 | state, int adapterPosition, IGroupSort item) { 84 | 85 | } 86 | 87 | @Override 88 | public void onDrawOverWhenLastGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 89 | int adapterPosition, IGroupSort item) { 90 | 91 | } 92 | 93 | @Override 94 | public void onDrawOverWhenSingleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State 95 | state, int adapterPosition, IGroupSort item) { 96 | 97 | } 98 | 99 | @Override 100 | public void getItemOffsetsWhenFirstGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 101 | state, int adapterPosition, IGroupSort item) { 102 | 103 | if (adapterPosition != 0) { 104 | //第一个群组不加顶部间距 105 | outRect.set(0, DensityUtil.dip2px(mContext, item.getGroupDividerSize()), 0, 0); 106 | } 107 | } 108 | 109 | @Override 110 | public void getItemOffsetsWhenMiddleGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 111 | state, int adapterPosition, IGroupSort item) { 112 | outRect.set(0, 0, 0, 0); 113 | } 114 | 115 | @Override 116 | public void getItemOffsetsWhenLastGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 117 | state, int adapterPosition, IGroupSort item) { 118 | outRect.set(0, 0, 0, 0); 119 | } 120 | 121 | @Override 122 | public void getItemOffsetsWhenSingleGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 123 | state, int adapterPosition, IGroupSort item) { 124 | outRect.set(0, DensityUtil.dip2px(mContext, item.getGroupDividerSize()), 0, 0); 125 | 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/GroupPressBackgroundDrawable.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.ColorFilter; 6 | import android.graphics.Paint; 7 | import android.graphics.Path; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.RectF; 10 | import android.graphics.drawable.Drawable; 11 | import android.support.annotation.NonNull; 12 | import android.support.annotation.Nullable; 13 | import android.view.View; 14 | 15 | /** 16 | * author: tc 17 | * date: 2019/9/15 & 11:12 18 | * version 1.0 19 | * description 动态生成带圆角的drawable 20 | * modify by 21 | */ 22 | public class GroupPressBackgroundDrawable extends Drawable { 23 | 24 | private Paint mPaint; 25 | private int backgroundColor = -1; 26 | private float[] corners; 27 | private RectF rectF; 28 | private Context mContext; 29 | private Path srcPath; 30 | 31 | public GroupPressBackgroundDrawable(int backgroundColor, float[] corners, View child, Context context) { 32 | this.backgroundColor = backgroundColor; 33 | this.corners = corners; 34 | mContext = context; 35 | srcPath = new Path(); 36 | mPaint = new Paint(); 37 | mPaint.setAntiAlias(true); 38 | rectF = new RectF(0, 0, child.getWidth(), child.getHeight()); 39 | } 40 | 41 | @Override 42 | public void draw(@NonNull Canvas canvas) { 43 | if (backgroundColor != -1) { 44 | mPaint.setColor(mContext.getResources().getColor(backgroundColor)); 45 | //默认无圆角 46 | srcPath.addRoundRect(rectF, corners, Path.Direction.CCW); 47 | canvas.drawPath(srcPath, mPaint); 48 | } 49 | 50 | } 51 | 52 | @Override 53 | public void setAlpha(int alpha) { 54 | 55 | } 56 | 57 | @Override 58 | public void setColorFilter(@Nullable ColorFilter colorFilter) { 59 | 60 | } 61 | 62 | @Override 63 | public int getOpacity() { 64 | return PixelFormat.OPAQUE; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/GroupSortItem.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.support.annotation.ColorRes; 4 | 5 | /** 6 | * author: tc 7 | * date: 2019/9/12 & 15:52 8 | * version 1.0 9 | * description 基础类,可以供参看使用,实际使用时只需要集成IGroupSort 10 | * modify by 11 | */ 12 | public abstract class GroupSortItem implements IGroupSort { 13 | private String groupType; 14 | private int groupBackgroundColorId; 15 | private int groupPressColorId; 16 | private int groupDividerSize; 17 | 18 | public GroupSortItem() { 19 | } 20 | 21 | public GroupSortItem(String groupType, int groupBackgroundColorId, int groupDividerSize, int groupPressColorId) { 22 | this.groupType = groupType; 23 | this.groupBackgroundColorId = groupBackgroundColorId; 24 | this.groupPressColorId = groupPressColorId; 25 | this.groupDividerSize = groupDividerSize; 26 | } 27 | 28 | 29 | public void setGroupType(String groupType) { 30 | this.groupType = groupType; 31 | } 32 | 33 | public void setGroupBackgroundColorId(@ColorRes int groupBackgroundColorId) { 34 | this.groupBackgroundColorId = groupBackgroundColorId; 35 | } 36 | 37 | public void setGroupPressColorId(@ColorRes int groupPressColorId) { 38 | this.groupPressColorId = groupPressColorId; 39 | } 40 | 41 | public void setGroupDividerSize(int groupDividerSize) { 42 | this.groupDividerSize = groupDividerSize; 43 | } 44 | 45 | @Override 46 | public String getGroupSortType() { 47 | return groupType; 48 | } 49 | 50 | @Override 51 | public int getGroupBackgroundColorId() { 52 | return groupBackgroundColorId; 53 | } 54 | 55 | @Override 56 | public int getGroupDividerSize() { 57 | return groupDividerSize; 58 | } 59 | 60 | @Override 61 | public int getGroupPressColorId() { 62 | return groupPressColorId; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/IGroupSort.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.support.annotation.ColorRes; 4 | 5 | /** 6 | * author: tc 7 | * date: 2019/9/12 & 15:10 8 | * version 1.0 9 | * description 实现本接口,以便完成ItemDecoration的分组逻辑处理以及绘制 10 | * modify by 11 | */ 12 | public interface IGroupSort { 13 | /** 14 | * 当前数据所在分组索引 15 | * 16 | * @return 17 | */ 18 | String getGroupSortType(); 19 | 20 | /** 21 | * 获取颜色id,用于 22 | * 23 | * @return 24 | */ 25 | @ColorRes 26 | int getGroupBackgroundColorId(); 27 | 28 | /** 29 | * item按压时的背景颜色 30 | * 31 | * @return 32 | */ 33 | @ColorRes 34 | int getGroupPressColorId(); 35 | 36 | /** 37 | * 获取item上下间距,单位为dp 38 | * 39 | * @return 40 | */ 41 | int getGroupDividerSize(); 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/ListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | import android.widget.Toast; 10 | 11 | import com.tc.bubblelayout.R; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * author: tc 17 | * date: 2019/9/11 & 10:58 18 | * version 1.0 19 | * description 20 | * modify by 21 | */ 22 | public class ListAdapter extends RecyclerView.Adapter { 23 | private List mList; 24 | private Context context; 25 | 26 | public ListAdapter(Context context, List list) { 27 | this.context = context; 28 | mList = list; 29 | } 30 | 31 | @Override 32 | public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 33 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_test_list, parent, false); 34 | return new BaseViewHolder(view); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(BaseViewHolder holder, int position) { 39 | final int adapterPosition = holder.getAdapterPosition(); 40 | TestGroupBean item = getItem(adapterPosition); 41 | if (item == null) { 42 | return; 43 | } 44 | holder.tv.setText(item.getName()); 45 | holder.itemView.setOnClickListener(new View.OnClickListener() { 46 | @Override 47 | public void onClick(View v) { 48 | notifyItemRemoved(adapterPosition); 49 | mList.remove(adapterPosition); 50 | notifyItemRangeChanged(adapterPosition, mList.size()); 51 | Toast.makeText(context, "delete index:" + adapterPosition, Toast.LENGTH_LONG).show(); 52 | } 53 | }); 54 | } 55 | 56 | private TestGroupBean getItem(int pos) { 57 | if (pos <= -1 || pos > getItemCount()) { 58 | return null; 59 | } 60 | return mList.get(pos); 61 | } 62 | 63 | @Override 64 | public int getItemCount() { 65 | if (mList == null) { 66 | return 0; 67 | } 68 | return mList.size(); 69 | } 70 | 71 | public class BaseViewHolder extends RecyclerView.ViewHolder { 72 | protected TextView tv; 73 | 74 | public BaseViewHolder(View itemView) { 75 | super(itemView); 76 | tv = itemView.findViewById(R.id.tv_test_list); 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/NormalDecoration.java: -------------------------------------------------------------------------------- 1 | // 2 | // Source code recreated from a .class file by IntelliJ IDEA 3 | // (powered by Fernflower decompiler) 4 | // 5 | 6 | package com.tc.bubblelayout.testrecylerview; 7 | 8 | import android.app.Activity; 9 | import android.graphics.Canvas; 10 | import android.graphics.Color; 11 | import android.graphics.Paint; 12 | import android.graphics.Paint.Align; 13 | import android.graphics.Paint.FontMetrics; 14 | import android.graphics.Rect; 15 | import android.support.v7.widget.RecyclerView; 16 | import android.support.v7.widget.RecyclerView.ItemDecoration; 17 | import android.support.v7.widget.RecyclerView.State; 18 | import android.util.SparseArray; 19 | import android.view.GestureDetector; 20 | import android.view.GestureDetector.OnGestureListener; 21 | import android.view.MotionEvent; 22 | import android.view.View; 23 | import android.view.View.OnTouchListener; 24 | 25 | import com.tc.bubblelayout.DensityUtil; 26 | import com.tc.bubblelayout.LogUtil; 27 | 28 | public abstract class NormalDecoration extends ItemDecoration { 29 | private static final String TAG = "NormalDecoration"; 30 | //是否关系排序 31 | private final boolean isSalutationSort; 32 | private Paint mHeaderTxtPaint = new Paint(1); 33 | private Paint mHeaderContentPaint; 34 | private int headerHeight; 35 | //不是字母排序是,Recycle距离顶部的距离 36 | private int noLetterHeaderHeight; 37 | private int textPaddingLeft; 38 | private int textSize = 34; 39 | private int textColor = Color.parseColor("#000000"); 40 | private final float txtYAxis; 41 | private RecyclerView mRecyclerView; 42 | private SparseArray stickyHeaderPosArray = new SparseArray(); 43 | private GestureDetector gestureDetector; 44 | private OnGestureListener gestureListener = new OnGestureListener() { 45 | @Override 46 | public boolean onDown(MotionEvent e) { 47 | return false; 48 | } 49 | 50 | @Override 51 | public void onShowPress(MotionEvent e) { 52 | } 53 | 54 | @Override 55 | public boolean onSingleTapUp(MotionEvent e) { 56 | return false; 57 | } 58 | 59 | @Override 60 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 61 | return false; 62 | } 63 | 64 | @Override 65 | public void onLongPress(MotionEvent e) { 66 | } 67 | 68 | @Override 69 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 70 | return false; 71 | } 72 | }; 73 | 74 | protected NormalDecoration(Activity activity, boolean isSalutationSort) { 75 | this.isSalutationSort = isSalutationSort; 76 | this.mHeaderTxtPaint.setColor(this.textColor); 77 | this.mHeaderTxtPaint.setTextSize((float) this.textSize); 78 | this.mHeaderTxtPaint.setTextAlign(Align.LEFT); 79 | this.mHeaderContentPaint = new Paint(1); 80 | int headerContentColor = Color.parseColor("#f5f5f5"); 81 | this.mHeaderContentPaint.setColor(headerContentColor); 82 | FontMetrics fontMetrics = this.mHeaderTxtPaint.getFontMetrics(); 83 | float total = -fontMetrics.ascent + fontMetrics.descent; 84 | this.txtYAxis = total / 2.0F - fontMetrics.descent; 85 | 86 | headerHeight = DensityUtil.dip2px(activity, 25); 87 | noLetterHeaderHeight = DensityUtil.dip2px(activity, 10); 88 | textPaddingLeft = DensityUtil.dip2px(activity, 25); 89 | } 90 | 91 | @Override 92 | public void getItemOffsets(Rect outRect, View itemView, RecyclerView parent, State state) { 93 | super.getItemOffsets(outRect, itemView, parent, state); 94 | if (this.mRecyclerView == null) { 95 | this.mRecyclerView = parent; 96 | } 97 | 98 | int currentCount; 99 | int pos = parent.getChildAdapterPosition(itemView); 100 | int itemCount = parent.getAdapter().getItemCount(); 101 | LogUtil.d(TAG, "itemCount--" + itemCount + ",getItemPos: " + pos); 102 | int applyContactCount = 0; 103 | String curHeaderName = this.getHeaderName(pos); 104 | if (curHeaderName != null) { 105 | if (pos == 0 || !curHeaderName.equals(this.getHeaderName(pos - 1))) { 106 | outRect.top = this.headerHeight; 107 | } 108 | } 109 | //字母排序联系人都有headName,除了好友申请item; 110 | //关系排序都没有headName 111 | if (curHeaderName != null) { 112 | // if (pos == 0 || !curHeaderName.equals(this.getHeaderName(pos - 1))) { 113 | // if (!curHeaderName.equals(this.getHeaderName(pos + 1))) { 114 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_all_selector); 115 | // } else { 116 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_top_selector); 117 | // } 118 | // } else if (!curHeaderName.equals(this.getHeaderName(pos + 1))) { 119 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_bottom_selector); 120 | // } else { 121 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_none_selector); 122 | // } 123 | } else { 124 | if (pos == 0) { 125 | outRect.top = this.noLetterHeaderHeight; 126 | } 127 | if (isSalutationSort) { 128 | currentCount = itemCount; 129 | } else { 130 | currentCount = applyContactCount; 131 | } 132 | // if (currentCount == 1) { 133 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_all_selector); 134 | // } else if (pos == 0) { 135 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_top_selector); 136 | // } else if (pos == currentCount - 1) { 137 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_bottom_selector); 138 | // } else { 139 | // itemView.setBackgroundResource(R.drawable.bg_list_item_corner_none_selector); 140 | // } 141 | } 142 | 143 | } 144 | 145 | public abstract String getHeaderName(int var1); 146 | 147 | @Override 148 | public void onDrawOver(Canvas canvas, RecyclerView recyclerView, State state) { 149 | super.onDrawOver(canvas, recyclerView, state); 150 | if (this.mRecyclerView == null) { 151 | this.mRecyclerView = recyclerView; 152 | } 153 | 154 | if (this.gestureDetector == null) { 155 | this.gestureDetector = new GestureDetector(recyclerView.getContext(), this.gestureListener); 156 | recyclerView.setOnTouchListener(new OnTouchListener() { 157 | @Override 158 | public boolean onTouch(View v, MotionEvent event) { 159 | return NormalDecoration.this.gestureDetector.onTouchEvent(event); 160 | } 161 | }); 162 | } 163 | 164 | this.stickyHeaderPosArray.clear(); 165 | int childCount = recyclerView.getChildCount(); 166 | int left = recyclerView.getLeft() + recyclerView.getPaddingLeft(); 167 | int right = recyclerView.getRight() - recyclerView.getPaddingRight(); 168 | String firstHeaderName = null; 169 | int translateTop = 0; 170 | 171 | for (int i = 0; i < childCount; ++i) { 172 | View childView = recyclerView.getChildAt(i); 173 | int pos = recyclerView.getChildAdapterPosition(childView); 174 | String curHeaderName = this.getHeaderName(pos); 175 | if (i == 0) { 176 | firstHeaderName = curHeaderName; 177 | } 178 | 179 | if (curHeaderName != null) { 180 | int viewTop = childView.getTop() + recyclerView.getPaddingTop(); 181 | if (pos == 0 || !curHeaderName.equals(this.getHeaderName(pos - 1))) { 182 | canvas.drawRect((float) left, (float) (viewTop - this.headerHeight), (float) right, 183 | (float) viewTop, this.mHeaderContentPaint); 184 | canvas.drawText(curHeaderName, (float) (left + this.textPaddingLeft), 185 | (float) (viewTop - this.headerHeight / 2) + this.txtYAxis, this.mHeaderTxtPaint); 186 | if (this.headerHeight < viewTop && viewTop <= 2 * this.headerHeight) { 187 | translateTop = viewTop - 2 * this.headerHeight; 188 | } 189 | 190 | this.stickyHeaderPosArray.put(pos, viewTop); 191 | } 192 | } 193 | } 194 | 195 | if (firstHeaderName != null) { 196 | canvas.save(); 197 | canvas.translate(0.0F, (float) translateTop); 198 | canvas.drawRect((float) left, 0.0F, (float) right, (float) this.headerHeight, this.mHeaderContentPaint); 199 | canvas.drawText(firstHeaderName, (float) (left + this.textPaddingLeft), 200 | (float) (this.headerHeight / 2) + this.txtYAxis, this.mHeaderTxtPaint); 201 | canvas.restore(); 202 | } 203 | } 204 | 205 | public void setTextSize(int textSize) { 206 | this.textSize = textSize; 207 | this.mHeaderTxtPaint.setTextSize((float) textSize); 208 | } 209 | 210 | public void setTextColor(int textColor) { 211 | this.textColor = textColor; 212 | this.mHeaderTxtPaint.setColor(textColor); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/PressEffectGroupItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Rect; 6 | import android.graphics.drawable.StateListDrawable; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | 10 | import com.tc.bubblelayout.DensityUtil; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * author: tc 16 | * date: 2019/9/14 & 16:29 17 | * version 1.0 18 | * description Item有触摸反馈颜色, 列表根据不同数据类型,拆分为多个群组进行布局显示的布局间隔绘制类 19 | * 20 | * @see IGroupSort 21 | *

22 | * modify by 23 | */ 24 | public class PressEffectGroupItemDecoration extends AbstractGroupItemDecoration { 25 | 26 | private int mCornerRadius; 27 | 28 | 29 | public PressEffectGroupItemDecoration(Context context, List list, int cornerRadius) { 30 | super(context, list); 31 | this.mCornerRadius = cornerRadius; 32 | } 33 | 34 | private void drawGroupCorner(Canvas canvas, View child, IGroupSort item, float[] corners) { 35 | if (item == null) { 36 | return; 37 | } 38 | StateListDrawable drawable = new StateListDrawable(); 39 | drawable.addState(new int[]{android.R.attr.state_pressed}, new GroupPressBackgroundDrawable( 40 | item.getGroupPressColorId(), corners, child, mContext)); 41 | //默认状态 42 | drawable.addState(new int[]{}, new GroupPressBackgroundDrawable( 43 | item.getGroupBackgroundColorId(), corners, child, mContext)); 44 | child.setBackgroundDrawable(drawable); 45 | } 46 | 47 | @Override 48 | public void onDrawWhenFirstGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 49 | int adapterPosition, IGroupSort item) { 50 | float[] corners = {mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 0, 0, 0, 0}; 51 | drawGroupCorner(canvas, child, item, corners); 52 | } 53 | 54 | 55 | @Override 56 | public void onDrawWhenMiddleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 57 | int adapterPosition, IGroupSort item) { 58 | float[] corners = {0, 0, 0, 0, 0, 0, 0, 0}; 59 | drawGroupCorner(canvas, child, item, corners); 60 | } 61 | 62 | @Override 63 | public void onDrawWhenLastGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, int 64 | adapterPosition, IGroupSort item) { 65 | float[] corners = {0, 0, 0, 0, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius}; 66 | drawGroupCorner(canvas, child, item, corners); 67 | } 68 | 69 | @Override 70 | public void onDrawWhenSingleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 71 | int adapterPosition, IGroupSort item) { 72 | float[] corners = {mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 73 | mCornerRadius, mCornerRadius}; 74 | drawGroupCorner(canvas, child, item, corners); 75 | } 76 | 77 | @Override 78 | public void onDrawOverWhenFirstGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State 79 | state, int adapterPosition, IGroupSort item) { 80 | 81 | } 82 | 83 | @Override 84 | public void onDrawOverWhenMiddleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State 85 | state, int adapterPosition, IGroupSort item) { 86 | 87 | } 88 | 89 | @Override 90 | public void onDrawOverWhenLastGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State state, 91 | int adapterPosition, IGroupSort item) { 92 | 93 | } 94 | 95 | @Override 96 | public void onDrawOverWhenSingleGroupItem(Canvas canvas, View child, RecyclerView parent, RecyclerView.State 97 | state, int adapterPosition, IGroupSort item) { 98 | 99 | } 100 | 101 | @Override 102 | public void getItemOffsetsWhenFirstGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 103 | state, int adapterPosition, IGroupSort item) { 104 | 105 | if (adapterPosition != 0) { 106 | //第一个群组不加顶部间距 107 | outRect.set(0, DensityUtil.dip2px(mContext, item.getGroupDividerSize()), 0, 0); 108 | } 109 | } 110 | 111 | @Override 112 | public void getItemOffsetsWhenMiddleGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 113 | state, int adapterPosition, IGroupSort item) { 114 | outRect.set(0, 0, 0, 0); 115 | } 116 | 117 | @Override 118 | public void getItemOffsetsWhenLastGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 119 | state, int adapterPosition, IGroupSort item) { 120 | outRect.set(0, 0, 0, 0); 121 | } 122 | 123 | @Override 124 | public void getItemOffsetsWhenSingleGroupItem(Rect outRect, View child, RecyclerView parent, RecyclerView.State 125 | state, int adapterPosition, IGroupSort item) { 126 | outRect.set(0, DensityUtil.dip2px(mContext, item.getGroupDividerSize()), 0, 0); 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/TestGroupBean.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.support.annotation.ColorRes; 4 | 5 | /** 6 | * author: tc 7 | * date: 2019/9/14 & 16:58 8 | * version 1.0 9 | * description 测试类 10 | * modify by 11 | */ 12 | public class TestGroupBean extends GroupSortItem { 13 | private String name; 14 | 15 | public String getName() { 16 | return name == null ? "" : name; 17 | } 18 | 19 | public void setName(String name) { 20 | this.name = name; 21 | } 22 | 23 | public TestGroupBean() { 24 | } 25 | 26 | public TestGroupBean(String groupType, @ColorRes int groupBackgroundColorId, int groupDividerSize, @ColorRes int 27 | groupPressColorId, String name) { 28 | super(groupType, groupBackgroundColorId, groupDividerSize, groupPressColorId); 29 | this.name = name; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "TestGroupBean{" + 35 | "name='" + name + '\'' + 36 | '}'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/TestListActivity.java: -------------------------------------------------------------------------------- 1 | package com.tc.bubblelayout.testrecylerview; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | 9 | import com.tc.bubblelayout.DensityUtil; 10 | import com.tc.bubblelayout.R; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * author: tc 17 | * date: 2019/9/11 & 10:57 18 | * version 1.0 19 | * description 20 | * modify by 21 | */ 22 | public class TestListActivity extends AppCompatActivity { 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_test_list); 27 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_list); 28 | final List list = new ArrayList<>(); 29 | 30 | for (int i = 0; i < 50; i++) { 31 | if (i < 4) { 32 | list.add(new TestGroupBean(String.valueOf(1), R.color.color_999999, 20, R.color.color_ebebeb, String 33 | .valueOf(i))); 34 | } else if (i == 4) { 35 | list.add(new TestGroupBean(String.valueOf(2), R.color.orange_ff6000, 15, R.color.color_ebebeb, String 36 | .valueOf(i))); 37 | } else if (i == 5) { 38 | list.add(new TestGroupBean(String.valueOf(3), R.color.colorAccent, 15, R.color.color_ebebeb, String 39 | .valueOf(i))); 40 | } else if (i < 20) { 41 | list.add(new TestGroupBean(String.valueOf(4), R.color.colorPrimaryDark, 10, R.color.color_ebebeb, 42 | String.valueOf(i))); 43 | } else { 44 | list.add(new TestGroupBean(String.valueOf(5), R.color.color_ffffff, 30, R.color.color_ebebeb, String 45 | .valueOf(i))); 46 | } 47 | } 48 | //测试群组间的间距为0的情况 49 | for (TestGroupBean testGroupBean : list) { 50 | testGroupBean.setGroupDividerSize(0); 51 | } 52 | // 数据要事先排好序 53 | // Collections.sort(list, new Comparator() { 54 | // @Override 55 | // public int compare(TestGroupBean o1, TestGroupBean o2) { 56 | // return 0; 57 | // } 58 | // }); 59 | final ListAdapter listAdapter = new ListAdapter(this, list); 60 | 61 | final NormalDecoration decoration = new NormalDecoration(this, true) { 62 | @Override 63 | public String getHeaderName(int pos) { 64 | if (pos <= -1 || pos >= listAdapter.getItemCount()) { 65 | return ""; 66 | } 67 | return list.get(pos).getGroupSortType(); 68 | } 69 | }; 70 | decoration.setTextSize(DensityUtil.dip2px(this, 17)); 71 | recyclerView.addItemDecoration(decoration); 72 | 73 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 74 | recyclerView.addItemDecoration(new PressEffectGroupItemDecoration(this, list, 25)); 75 | recyclerView.setAdapter(listAdapter); 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/java/com/tc/bubblelayout/testrecylerview/readme: -------------------------------------------------------------------------------- 1 | 封装一个分组展示圆角形状的功能,可以学习下ItemDecoration的使用 -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/drawable-xhdpi/pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/drawable-xhdpi/pic1.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/drawable-xhdpi/white_sound_wave_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/drawable-xhdpi/white_sound_wave_default.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 24 | 25 | 34 | 35 | 39 | 40 | 47 | 48 | 49 | 50 | 55 | 56 | 69 | 70 | 75 | 76 | 77 | 84 | 85 | 90 | 91 | 92 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/layout/activity_test_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/layout/include_pop_emoji_bubble.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/layout/item_test_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #ffffff 7 | #cccccc 8 | #999999 9 | #ff6000 10 | #ffffff 11 | #e6ffffff 12 | #e6cbcbcb 13 | #ebebeb 14 | 15 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BubbleLayout 3 | 4 | -------------------------------------------------------------------------------- /BubbleLayout/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /BubbleLayout/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.0.1' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /BubbleLayout/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /BubbleLayout/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/389273716/BubbleLayout/9df9014745f980fa9b7ce17a350dc6e20d073580/BubbleLayout/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /BubbleLayout/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Sep 12 16:16:12 CST 2018 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-4.1-all.zip 7 | -------------------------------------------------------------------------------- /BubbleLayout/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 | -------------------------------------------------------------------------------- /BubbleLayout/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 | -------------------------------------------------------------------------------- /BubbleLayout/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BubbleLayout 2 | 自定义气泡布局,支持文本、图片、多种类型混合 3 | 4 | [博客说明](https://www.jianshu.com/p/19929fabedca "title") 5 | --------------------------------------------------------------------------------