diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..518c6bc --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 24e8552..8994e32 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,6 @@ -#Wed May 07 09:31:48 CST 2025 +#Mon Sep 22 21:05:11 CST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -#distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -#distributionUrl=file:///D:/Greadle/gradle-8.10.2-all.zip -distributionUrl=file:///D:/Gradle/gradle-8.10.2-bin.zip +zipStorePath=wrapper/distsl. diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/view/AvatarWithDecoration.java b/moduleUtil/src/main/java/com/xscm/moduleutil/view/AvatarWithDecoration.java new file mode 100644 index 0000000..3112883 --- /dev/null +++ b/moduleUtil/src/main/java/com/xscm/moduleutil/view/AvatarWithDecoration.java @@ -0,0 +1,119 @@ +package com.xscm.moduleutil.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import androidx.appcompat.widget.AppCompatImageView; + +import com.xscm.moduleutil.R; + +public class AvatarWithDecoration extends AppCompatImageView { + // 挂件图片 + private Drawable decoration; + // 挂件位置偏移量 + private int decorationOffsetX = 0; + private int decorationOffsetY = 0; + // 挂件大小比例(相对于头像) + private float decorationScale = 0.3f; + + public AvatarWithDecoration(Context context) { + super(context); + init(); + } + + public AvatarWithDecoration(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public AvatarWithDecoration(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AvatarWithDecoration); + decoration = a.getDrawable(R.styleable.AvatarWithDecoration_decoration); + decorationScale = a.getFloat(R.styleable.AvatarWithDecoration_decorationScale, 0.3f); + decorationOffsetX = a.getInt(R.styleable.AvatarWithDecoration_decorationOffsetX, 0); + decorationOffsetY = a.getInt(R.styleable.AvatarWithDecoration_decorationOffsetY, 0); + a.recycle(); + } + + private void init() { + // 可以在这里设置默认的挂件 + decoration = getResources().getDrawable(R.mipmap.xlh_image); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // 如果有挂件,则绘制挂件 + if (decoration != null) { + drawDecoration(canvas); + } + } + + private void drawDecoration(Canvas canvas) { + // 获取头像的宽高 + int avatarWidth = getWidth(); + int avatarHeight = getHeight(); + + // 计算挂件的大小 + int decorationWidth = (int) (avatarWidth * decorationScale); + int decorationHeight = (int) (avatarHeight * decorationScale); + + // 计算挂件的位置(右下角) + int left = avatarWidth - decorationWidth + decorationOffsetX; + int top = avatarHeight - decorationHeight + decorationOffsetY; + int right = left + decorationWidth; + int bottom = top + decorationHeight; + + // 设置挂件的绘制边界 + decoration.setBounds(left, top, right, bottom); + + // 绘制挂件 + decoration.draw(canvas); + } + + /** + * 设置挂件图片 + */ + public void setDecoration(Drawable decoration) { + this.decoration = decoration; + invalidate(); + } + + /** + * 设置挂件图片(通过Bitmap) + */ + public void setDecoration(Bitmap bitmap) { + if (bitmap != null) { + this.decoration = new BitmapDrawable(getResources(), bitmap); + invalidate(); + } + } + + /** + * 设置挂件位置偏移量 + */ + public void setDecorationOffset(int offsetX, int offsetY) { + this.decorationOffsetX = offsetX; + this.decorationOffsetY = offsetY; + invalidate(); + } + + /** + * 设置挂件大小比例 + */ + public void setDecorationScale(float scale) { + if (scale > 0 && scale <= 1) { + this.decorationScale = scale; + invalidate(); + } + } +} + diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/view/FashionAvatarView.java b/moduleUtil/src/main/java/com/xscm/moduleutil/view/FashionAvatarView.java new file mode 100644 index 0000000..e0ab8c0 --- /dev/null +++ b/moduleUtil/src/main/java/com/xscm/moduleutil/view/FashionAvatarView.java @@ -0,0 +1,373 @@ +package com.xscm.moduleutil.view; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; +import com.xscm.moduleutil.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class FashionAvatarView extends View { + // 头像相关属性 + private Bitmap mAvatarBitmap; + private String mAvatarUrl; + private Drawable mPlaceholderAvatar; + private int mAvatarRadius; + private int mAvatarBorderWidth; + private int mAvatarBorderColor; + + // 顶部标签属性 + private String mTagText; + private int mTagTextColor; + private float mTagTextSize; + private int mTagBackgroundColor; + private float mTagCornerRadius; + private int mTagPadding; + private int mTagOffsetY; // 标签Y轴偏移量 + + // 底部文字属性 + private String mBottomText; + private int mBottomTextColor; + private float mBottomTextSize; + private int mBottomTextOffsetY; // 底部文字Y轴偏移量 + + // 爱心装饰属性 + private Drawable mHeartIcon; + private int mHeartCount; + private int mHeartSize; + private List mHeartPositions = new ArrayList<>(); + private Random mRandom = new Random(); + + // 画笔 + private Paint mAvatarPaint; + private Paint mTextPaint; + private Paint mTagBgPaint; + + public FashionAvatarView(Context context) { + this(context, null); + } + + public FashionAvatarView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public FashionAvatarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initAttrs(context, attrs); + initPaints(); + } + + private void initAttrs(Context context, AttributeSet attrs) { + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FashionAvatarView); + + // 头像属性 + mAvatarUrl = ta.getString(R.styleable.FashionAvatarView_avatarUrl); + mPlaceholderAvatar = ta.getDrawable(R.styleable.FashionAvatarView_placeholderAvatar); + mAvatarRadius = ta.getDimensionPixelSize(R.styleable.FashionAvatarView_avatarRadius, dp2px(60)); + mAvatarBorderWidth = ta.getDimensionPixelSize(R.styleable.FashionAvatarView_avatarBorderWidth, dp2px(2)); + mAvatarBorderColor = ta.getColor(R.styleable.FashionAvatarView_avatarBorderColor, Color.parseColor("#FFD700")); + + // 顶部标签属性 + mTagText = ta.getString(R.styleable.FashionAvatarView_tagText); + mTagTextColor = ta.getColor(R.styleable.FashionAvatarView_tagTextColor, Color.WHITE); + mTagTextSize = ta.getDimension(R.styleable.FashionAvatarView_tagTextSize, sp2px(12)); + mTagBackgroundColor = ta.getColor(R.styleable.FashionAvatarView_tagBackgroundColor, Color.parseColor("#FFA500")); + mTagCornerRadius = ta.getDimension(R.styleable.FashionAvatarView_tagCornerRadius, dp2px(4)); + mTagPadding = ta.getDimensionPixelSize(R.styleable.FashionAvatarView_tagPadding, dp2px(4)); + mTagOffsetY = ta.getDimensionPixelSize(R.styleable.FashionAvatarView_tagOffsetY, 0); + + // 底部文字属性 + mBottomText = ta.getString(R.styleable.FashionAvatarView_bottomText); + mBottomTextColor = ta.getColor(R.styleable.FashionAvatarView_bottomTextColor, Color.WHITE); + mBottomTextSize = ta.getDimension(R.styleable.FashionAvatarView_bottomTextSize, sp2px(14)); + mBottomTextOffsetY = ta.getDimensionPixelSize(R.styleable.FashionAvatarView_bottomTextOffsetY, dp2px(10)); + + // 爱心装饰属性 + mHeartIcon = ta.getDrawable(R.styleable.FashionAvatarView_heartIcon); + if (mHeartIcon == null) { + try { + mHeartIcon = context.getResources().getDrawable(R.mipmap.xlh_image); + } catch (Exception e) { + e.printStackTrace(); + } + } + mHeartCount = ta.getInt(R.styleable.FashionAvatarView_heartCount, 6); + mHeartSize = ta.getDimensionPixelSize(R.styleable.FashionAvatarView_heartSize, dp2px(16)); + + ta.recycle(); + + // 加载头像 + if (mAvatarUrl != null && !mAvatarUrl.isEmpty()) { + loadAvatarFromNetwork(); + } else if (mPlaceholderAvatar != null) { + mAvatarBitmap = drawableToBitmap(mPlaceholderAvatar); + } + } + + private void initPaints() { + // 头像画笔 + mAvatarPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mAvatarPaint.setDither(true); + + // 文字画笔 + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTextAlign(Paint.Align.CENTER); + + // 标签背景画笔 + mTagBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTagBgPaint.setColor(mTagBackgroundColor); + } + + private void loadAvatarFromNetwork() { + Glide.with(this) + .asBitmap() + .load(mAvatarUrl) + .into(new SimpleTarget() { + @Override + public void onResourceReady(Bitmap resource, Transition transition) { + mAvatarBitmap = resource; + invalidate(); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + super.onLoadFailed(errorDrawable); + if (mPlaceholderAvatar != null) { + mAvatarBitmap = drawableToBitmap(mPlaceholderAvatar); + } + invalidate(); + } + }); + } + + private Bitmap drawableToBitmap(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + if (drawable == null) { + return null; + } + + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + + if (width <= 0) width = mAvatarRadius * 2; + if (height <= 0) height = mAvatarRadius * 2; + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + private void initHeartPositions() { + mHeartPositions.clear(); + + if (mHeartIcon == null || mHeartCount <= 0 || mHeartSize <= 0) { + return; + } + + int centerX = getWidth() / 2; + int centerY = getAvatarCenterY(); + // 爱心围绕的半径,比头像大一些 + int radius = mAvatarRadius + mAvatarBorderWidth + mHeartSize / 2; + + for (int i = 0; i < mHeartCount; i++) { + // 随机分布在头像周围 + double angle = 2 * Math.PI * mRandom.nextDouble(); + // 稍微随机调整距离,让分布更自然 + float distanceFactor = 0.8f + mRandom.nextFloat() * 0.4f; + + float x = (float) (centerX + radius * distanceFactor * Math.cos(angle)); + float y = (float) (centerY + radius * distanceFactor * Math.sin(angle)); + mHeartPositions.add(new PointF(x, y)); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // 计算宽度:直径 + 左右可能的爱心空间 + int width = 2 * (mAvatarRadius + mAvatarBorderWidth + mHeartSize / 2); + + // 计算高度:头像直径 + 标签高度 + 底部文字高度 + 间距 + int tagHeight = (int) (mTagTextSize + mTagPadding * 2); + int bottomTextHeight = (int) mBottomTextSize; + int height = 2 * (mAvatarRadius + mAvatarBorderWidth) + + tagHeight / 2 // 标签一半在头像内 + + bottomTextHeight + mBottomTextOffsetY + + dp2px(10); + + setMeasuredDimension(resolveSize(width, widthMeasureSpec), + resolveSize(height, heightMeasureSpec)); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // 视图大小确定后计算爱心位置 + initHeartPositions(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (getWidth() == 0 || getHeight() == 0) { + return; + } + + int centerX = getWidth() / 2; + int avatarCenterY = getAvatarCenterY(); + + // 1. 绘制爱心装饰 + drawHearts(canvas); + + // 2. 绘制头像边框 + mAvatarPaint.setColor(mAvatarBorderColor); + mAvatarPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(centerX, avatarCenterY, mAvatarRadius + mAvatarBorderWidth, mAvatarPaint); + + // 3. 绘制头像 + if (mAvatarBitmap != null) { + mAvatarPaint.setShader(new BitmapShader(mAvatarBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); + canvas.drawCircle(centerX, avatarCenterY, mAvatarRadius, mAvatarPaint); + mAvatarPaint.setShader(null); + } + + // 4. 绘制顶部标签 + drawTag(canvas, centerX, avatarCenterY); + + // 5. 绘制底部文字 + drawBottomText(canvas, centerX); + } + + private int getAvatarCenterY() { + // 计算头像中心Y坐标,考虑标签的高度 + int tagHeight = (int) (mTagTextSize + mTagPadding * 2); + return mAvatarRadius + mAvatarBorderWidth + tagHeight / 2 + mTagOffsetY; + } + + private void drawHearts(Canvas canvas) { + if (mHeartIcon == null || mHeartPositions.isEmpty()) { + return; + } + + canvas.save(); + for (PointF point : mHeartPositions) { + int left = (int) (point.x - mHeartSize / 2); + int top = (int) (point.y - mHeartSize / 2); + int right = left + mHeartSize; + int bottom = top + mHeartSize; + + // 只绘制在视图范围内的爱心 + if (right > 0 && bottom > 0 && left < getWidth() && top < getHeight()) { + mHeartIcon.setBounds(left, top, right, bottom); + mHeartIcon.draw(canvas); + } + } + canvas.restore(); + } + + private void drawTag(Canvas canvas, int centerX, int avatarCenterY) { + if (mTagText == null || mTagText.isEmpty()) { + return; + } + + // 计算标签文字宽度 + mTextPaint.setTextSize(mTagTextSize); + float textWidth = mTextPaint.measureText(mTagText); + + // 计算标签背景矩形 + float tagLeft = centerX - textWidth / 2 - mTagPadding; + float tagRight = centerX + textWidth / 2 + mTagPadding; + // 标签底部与头像顶部对齐 + float tagBottom = avatarCenterY - mAvatarRadius - mAvatarBorderWidth; + float tagTop = tagBottom - mTagTextSize - mTagPadding * 2; + + RectF tagRect = new RectF(tagLeft, tagTop, tagRight, tagBottom); + + // 绘制标签背景 + canvas.drawRoundRect(tagRect, mTagCornerRadius, mTagCornerRadius, mTagBgPaint); + + // 绘制标签文字 + mTextPaint.setColor(mTagTextColor); + Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); + float baseLineY = tagBottom - mTagPadding - fontMetrics.bottom; + canvas.drawText(mTagText, centerX, baseLineY, mTextPaint); + } + + private void drawBottomText(Canvas canvas, int centerX) { + if (mBottomText == null || mBottomText.isEmpty()) { + return; + } + + // 计算文字位置:头像底部下方 + int textY = getAvatarCenterY() + mAvatarRadius + mAvatarBorderWidth + mBottomTextOffsetY; + + mTextPaint.setColor(mBottomTextColor); + mTextPaint.setTextSize(mBottomTextSize); + + Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); + float baseLineY = textY - fontMetrics.top; + + canvas.drawText(mBottomText, centerX, baseLineY, mTextPaint); + } + + // dp转px + private int dp2px(float dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, + getResources().getDisplayMetrics()); + } + + // sp转px + private float sp2px(float sp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, + getResources().getDisplayMetrics()); + } + + // 设置器方法 + public void setAvatarUrl(String url) { + this.mAvatarUrl = url; + loadAvatarFromNetwork(); + } + + public void setTagText(String text) { + this.mTagText = text; + invalidate(); + } + + public void setBottomText(String text) { + this.mBottomText = text; + invalidate(); + } + + public void setHeartIcon(Drawable heartIcon) { + this.mHeartIcon = heartIcon; + initHeartPositions(); + invalidate(); + } +} + diff --git a/moduleUtil/src/main/res/drawable/bg_person.xml b/moduleUtil/src/main/res/drawable/bg_person.xml new file mode 100644 index 0000000..a7b3cf2 --- /dev/null +++ b/moduleUtil/src/main/res/drawable/bg_person.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/moduleUtil/src/main/res/drawable/bg_round_corner.xml b/moduleUtil/src/main/res/drawable/bg_round_corner.xml new file mode 100644 index 0000000..298a617 --- /dev/null +++ b/moduleUtil/src/main/res/drawable/bg_round_corner.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/moduleUtil/src/main/res/layout/fragment_tour_club_dialog.xml b/moduleUtil/src/main/res/layout/fragment_tour_club_dialog.xml index 2ed1185..687dbf4 100644 --- a/moduleUtil/src/main/res/layout/fragment_tour_club_dialog.xml +++ b/moduleUtil/src/main/res/layout/fragment_tour_club_dialog.xml @@ -62,6 +62,7 @@ app:layout_constraintTop_toBottomOf="@+id/tv_gz" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -281,7 +432,7 @@ android:layout_marginBottom="@dimen/dp_15" android:orientation="horizontal" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintTop_toBottomOf="@+id/cl_gift" + app:layout_constraintTop_toBottomOf="@+id/gift_l5" app:layout_constraintBottom_toTopOf="@+id/exchange_layout" /> @@ -325,6 +476,7 @@ app:layout_constraintEnd_toStartOf="@+id/tv_option" app:layout_constraintTop_toTopOf="@+id/exchange_layout" app:layout_constraintBottom_toBottomOf="@+id/exchange_layout" + android:visibility="gone" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/moduleroom/src/main/res/layout/activity_room.xml b/moduleroom/src/main/res/layout/activity_room.xml index 15786cf..772b416 100644 --- a/moduleroom/src/main/res/layout/activity_room.xml +++ b/moduleroom/src/main/res/layout/activity_room.xml @@ -482,7 +482,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" tools:visibility="visible" /> - + +