English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
모바일 기기에서悬浮窗 애플리케이션은 점점 더 많아지고 있습니다. 사용자에게 가장 일반적인悬浮窗 애플리케이션은 보안 소프트웨어의悬浮소형 컨트롤입니다.360가드에게서,悬浮窗이 열릴 때,그것은 작은 구름입니다,구름을 드래그할 수 있으며,구름을 클릭하면 대형 윈도우 컨트롤이 나타나고,메모리 해제와 같은 추가적인 작업을 수행할 수 있습니다. 그래서 모바일 애플리케이션을 따라 구현했습니다.360가속구름, 클릭하여 메모리를 해제할 수 있는 기능을 추가했습니다.
모바일 기기의 경우 스크린 캡처 만 있기 때문에 다음과 같이 구현되었습니다: 오픈 버튼을 클릭하면 터치 스크린에 나타나는悬浮窗小球控件에 사용 가능한 메모리 비율이 표시됩니다; 드래그 중에 드래그 중에 터치 스크린에 나타나는悬浮窗小球控件이 안드로이드 아이콘으로 변환됩니다; 드래그를 멈추면 터치 스크린의 양쪽에悬浮窗小球控件이 붙습니다; 터치 스크린을 클릭하면 터치 스크린의 하단에 대형 윈도우 컨트롤이 나타납니다, 그 안의 터치 스크린을 클릭하면 터치 스크린의 메모리를 해제할 수 있습니다; 터치 스크린의 다른 부분을 클릭하면 대형 윈도우 컨트롤이 사라지고 터치 스크린이 다시 나타납니다.
효과는 다음과 같습니다:
다음은 구현된 중요한 단계입니다:
1.FloatCircleView 구현(정의하는 View)
FloatCircleView를 구현하는 과정이 정의하는 과정입니다.1View의 속성을 정의합니다 2View의 생성자에서 우리가 정의한 속성을 얻습니다 3onMesure를 재정의합니다 4onDraw를 재정의합니다. 다른 속성을 정의하지 않았기 때문에 많은 단계를 건너뛰었습니다.
다양한 변수의 초기화, 드래그 중에 표시할 아이콘 설정 및 다양한 메모리 계산。(빛그림자에 표시용)
public int width=100; public int heigth=100; private Paint circlePaint;//원 그리기 private Paint textPaint; //문자 그리기 private float availMemory; //사용 가능한 메모리 private float totalMemory; //총 메모리 private String text; //표시되는 사용 가능한 메모리 비율 private boolean isDraging=false; //휴대폰을 드래그 중인지 여부. private Bitmap src; private Bitmap scaledBitmap; //확대된 이미지. /** * 초기화화면의 펜을 설정하고 사용 가능한 메모리, 총 메모리 및 사용 가능한 메모리 비율을 계산합니다. */ public void initPatints() { circlePaint = new Paint(); circlePaint.setColor(Color.CYAN); circlePaint.setAntiAlias(true); textPaint = new Paint(); textPaint.setColor(Color.WHITE); textPaint.setTextSize(25); textPaint.setFakeBoldText(true); textPaint.setAntiAlias(true); //이미지를 설정합니다 src = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); //스케일된 이미지(아이콘과悬浮小球의 크기와 같게 설정된 것) scaledBitmap = Bitmap.createScaledBitmap(src, width, heigth, true); //内存使用量, 전체 메모리, 사용 가능 메모리 비율을 계산합니다. availMemory= (float) getAvailMemory(getContext()); totalMemory= (float) getTotalMemory(getContext()); text=(int)((availMemory/totalMemory)*100)+"%"; }
onMeasure();은 고정된 너비와 높이를 고정하며, setMeasuredDimension(width, heigth);를 통해 전달합니다.
onDraw();을 호출하여悬浮小球을 그립니다. boolean 변수를 정의하여 현재 상태가 드래그 중 작은 공 상태인지 확인합니다. 드래그 중 작은 공 상태면 해당 위치에 android 아이콘을 그립니다. 드래그 중이 아니면 작은 공을 그립니다. 작은 공을 그리는 것은 어렵지 않지만, 글자를 그리는 것이 중요합니다. 다음은2그림은 글자를 그릴 때의 이해를 더욱 깊게 합니다.
1.글자를 그릴 때의 x 좌표(1.textPaint.measureText(text); 글자 너비를 얻습니다2.작은 공의 너비/2-글자 너비/2).
2.글자를 그릴 때의 y 좌표(1.Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); 글자 속성 측정 클래스를 얻습니다.2.(fontMetrics.ascent + fontMetrics.descent) / 2 글자 높이를 얻습니다.3.작은 공의 높이/2-글자 높이/2)
그림 하나로 충분히 이해할 수 있습니다:
/** * 작은 공 및 텍스트를 그립니다. 작은 공이 드래그 중이면 android 아이콘을 표시하고, 아니면 작은 공을 표시합니다. * @param canvas */ @Override protected void onDraw(Canvas canvas) { if (isDraging){ canvas.drawBitmap(scaledBitmap,0,0,null); } else { //1.화면을 둘러싸는 원 canvas.drawCircle(width) / 2, heigth / 2, width / 2, circlePaint); //2.drawText(text float textwidth = textPaint.measureText(text);//텍스트 너비 float x = width / 2 - textwidth / 2; Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float dy = -(fontMetrics.ascent + fontMetrics.descent) / 2; float y = heigth / 2 + dy; canvas.drawText(text, x, y, textPaint); } }
폰의 사용된 메모리 및 총 메모리를 얻는 방법:
public long getAvailMemory(Context context) { // android 현재 사용 가능한 메모리 크기를 가져옵니다 ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); am.getMemoryInfo(mi); //mi.availMem; 현재 시스템의 사용 가능한 메모리 //return Formatter.formatFileSize(context, mi.availMem);// 가져온 메모리 크기를 규격화합니다 return mi.availMem/(1024*1024); } public long getTotalMemory(Context context) { String str1 = "/proc/meminfo";// 시스템 메모리 정보 파일 String str2; String[] arrayOfString; long initial_memory = 0; try { FileReader localFileReader = new FileReader(str1); BufferedReader localBufferedReader = new BufferedReader( localFileReader, 8192); str2 = localBufferedReader.readLine();// meminfo 첫 번째 행을 읽어서 시스템 총 메모리 크기를 얻습니다. arrayOfString = str2.split("\\s+); for (String num : arrayOfString) { Log.i(str2, num + "\t"); } initial_memory = Integer.valueOf(arrayOfString[1]).intValue() * 1024;// 시스템 총 메모리를 얻습니다. 단위는 KB, 곱합니다1024Byte 변환 localBufferedReader.close(); } } //return Formatter.formatFileSize(context, initial_memory);// Byte을 KB나 MB로 변환하여 메모리 크기를 규격화합니다 return initial_memory/(1024*1024); }
2. WindowManager 창 관리 클래스를 생성하여 가변小球과 하단 대 창을 관리합니다.
WindowManager 클래스. 전체 가변小球과 휴대폰 하단 대 창의 표시 및 숨기기를 관리합니다.
Manifest 파일에 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> 권한。
WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);를 통해 창 관리 클래스를 가져옵니다;
wm.addView(view, params);를 사용하여 view를 창에 추가합니다.
wm.remove(view,params);를 사용하여 view를 창에서 제거합니다.
wm.updateViewLayout(view,params);를 사용하여 view를 업데이트합니다.
WindowManager.LayoutParams는 view의 다양한 속성을 설정하는 데 사용됩니다.
1. FloatViewManager 인스턴스를 생성합니다.
//단말 모델 생성 public static FloatViewManager getInstance(Context context){ if (inStance==null){ synchronized(FloatViewManager.class){ if (inStance==null){ inStance=new FloatViewManager(context); } } } return inStance; }
2.悬浮小球展示和底部窗口展示的方法。(展示窗口的方法与展示悬浮小球类似。)
/** * 浮动窗口展示 */ public void showFloatCircleView(){ //파라미터 설정 if (params==null){ params = new WindowManager.LayoutParams(); //가로, 세로 params.width=circleView.width; params.height=circleView.heigth; //정렬 방식 params.gravity= Gravity.TOP|Gravity.LEFT; //이동량 params.x=0; params.y=0; //타입 params.type=WindowManager.LayoutParams.TYPE_TOAST; //이 window 속성을 설정합니다. params.flags= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; //픽셀 포맷 params.format= PixelFormat.RGBA_8888; } //원을 창에 추가합니다. wm.addView(circleView, params); } public void showFloatCircleView(){ ...... }
3. 프로그램을 시작할 때, 첫 번째로��창 원을 생성합니다. 원은 드래그가 가능하며, 원을 클릭하면 휴대폰 하단 창( FloatMenuView)이 표시되고, 원이 숨깁니다. 따라서, 원(circleView)에 대해 setOnTouchListener와 setOnClickListener 이벤트 리스너를 설정해야 합니다.
원의 이벤트 분배를 분석합니다; 원에 대해:
ACTION_DOWN이 발생할 때, 원의 downX, downY, startX, startY를 기록합니다.
ACTION_MOVE이 발생할 때, circleView의 드래그 상태를 true로 설정하고, 원의 moveX, moveY를 기록합니다. 원의 이동 거리를 계산한 후 wm.updateViewLayout(circleView, params);을 통해 원의 위치를 업데이트합니다. 마지막으로 마지막 move 좌표를 startX, startY에 할당합니다.
ACTION_UP이 발생할 때, circleView의 드래그 여부를 false로 설정하고, 올릴 때의 좌표, upx를 기록합니다. upx와 휴대폰 화면 너비를 기준으로/2,进行判断,来觉得最终小球是贴在屏幕左侧,还是右侧。后面为小球拖拽的误差。当小球拖拽的距离小于10个像素时,可以触发小球的点击事件。(小球的Touch事件,优先于小球的点击事件,当Touch事件返回true时,此事件被消费,不再向下传递事件。当Touch事件返回false时,此事件继续向下传递,从而触发小球的点击事件。)
小球的点击事件:点击小球,悬浮小球隐藏,手机底部窗体出现。并设置有底部窗体出现时的过渡动画。
//给circleView设置touch监听。 private View.OnTouchListener circleViewOnTouchListener=new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: //最后按下时的坐标,根据ACTION_MOVE理解。 startX = event.getRawX(); startY = event.getRawY(); //按下时的坐标。 downX = event.getRawX(); downY = event.getRawY(); break; case MotionEvent.ACTION_MOVE: circleView.setDrageState(true); moveX = event.getRawX(); moveY=event.getRawY(); float dx = moveX -startX; float dy=moveY-startY; params.x+=dx; params.y+=dy; wm.updateViewLayout(circleView,params); startX= moveX; startY=moveY; break; case MotionEvent.ACTION_UP: float upx=event.getRawX(); if (upx>getScreenWidth();/2) { params.x=getScreenWidth();-circleView.width; } else { params.x=0; } circleView.setDrageState(false); wm.updateViewLayout(circleView,params); if (Math.abs(moveX-downX)>10) { return true; } else { return false; } default: break; } return false; } }); circleView.setOnTouchListener(circleViewOnTouchListener); circleView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Toast.makeText(, "onclick", Toast.LENGTH_SHORT).show(); //circleView를 숨기고 메뉴 표시줄을 표시합니다。 wm.removeView(circleView); showFloatMenuView(); floatMenuView.startAnimation(); } });
3. MyProgreeView(모바일 하단 창에 있는 구름 구현)。
1. 페인트 초기화, 뷰에 터치 이벤트를 감지합니다. 클릭과 더블 클릭 이벤트를 감지합니다。(뷰를 클릭할 수 있게 설정해야 합니다)
private void initPaint() { //원 그리기 페인트 circlepaint = new Paint(); circlepaint.setColor(Color.argb(0xff, 0x3a, 0x8c, 0x6c)); circlepaint.setAntiAlias(true); //진행 표시선 페인트 progerssPaint = new Paint(); progerssPaint.setAntiAlias(true); progerssPaint.setColor(Color.argb(0xff, 0x4e, 0xcc, 0x66)); progerssPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//중복 부분 그리기 //진행도 페인트 textPaint = new Paint(); textPaint.setAntiAlias(true); textPaint.setColor(Color.WHITE); textPaint.setTextSize(25); //화면 bitmap = Bitmap.createBitmap(width, heigth, Bitmap.Config.ARGB_8888); bitmapCanvas = new Canvas(bitmap); //手动监听. gestureDetector = new GestureDetector(new MyGertureDetectorListener()); setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return gestureDetector.onTouchEvent(event); } }); //view를 클릭할 수 있게 설정. setClickable(true); } class MyGertureDetectorListener extends GestureDetector.SimpleOnGestureListener{ @Override public boolean onDoubleTap(MotionEvent e) { ...... //두 번 클릭 이벤트의 로직 return super.onDoubleTap(e); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { ...... //단일 클릭 이벤트의 로직 return super.onSingleTapConfirmed(e); } }
2. 핸들러를 통해 클릭과 두 번 클릭 이벤트의 상태를 업데이트합니다. 클릭 시, 베塞尔 곡선을 사용하여 파동 흔들림 효과를 구현합니다. 두 번 클릭 시, 파동이 계속 내려가며 메모리 해제가 이루어지고, 마지막으로 메모리 해제 후 사용된 메모리 비율을 표시합니다. 핸들러가 주기적인 메시지를 전송하여 단일 클릭 이벤트와 두 번 클릭 이벤트의 구체를 지속적으로 다시 그립니다. (다시 그리기는 다음 절에서 설명합니다).
//단일 클릭 이벤트 전송 주기를 핸들러. private void startSingleTapAnimation() { handler.postDelayed(singleTapRunnable,200); } private SingleTapRunnable singleTapRunnable=new SingleTapRunnable(); class SingleTapRunnable implements Runnable{ @Override public void run() { count--; if (count>=0) { invalidate();//지속적으로 다시 그립니다. handler.postDelayed(singleTapRunnable,200); } else { handler.removeCallbacks(singleTapRunnable); count=50; } } } //두 번 클릭 이벤트 전송 주기를 핸들러. private void startDoubleTapAnimation() { handler.postDelayed(runnbale,50); } private DoubleTapRunnable runnbale=new DoubleTapRunnable(); class DoubleTapRunnable implements Runnable{ @Override public void run() { num--; if (num>=0){ invalidate();//지속적으로 다시 그립니다. handler.postDelayed(runnbale,50); } else { handler.removeCallbacks(runnbale); //메모리를 해제합니다. killprocess(); //해제된 후 사용된 메모리 비율을 계산합니다. num=(int)(((float)currentProgress/max)*100); } } }
3을 다시 그립니다.
최초로 공을 그리기 시작합니다. 물파 경로의 그리기와 동일합니다. //을 그립니다 bitmapCanvas.drawCircle(width / 2, heigth / 2, width / 2, circlepaint); //path를 기준으로 물파 경로를 그립니다. 그릴 전에 항상 이전의 path.reset()를 호출합니다. path.reset(); float y =(1-(float)num/100)*heigth; path.moveTo(width, y); path.lineTo(width, heigth); path.lineTo(0, heigth); path.lineTo(0, y);
그리고 베塞尔 곡선을 사용하여 물파 경로를 그립니다.
Android-베塞尔 곡선
에서 베塞尔 곡선의 응용
이곳에는 베塞尔 곡선에 대한 자세한 설명이 있습니다. 실제로 깊이 이해할 필요는 없습니다. 물파 효과를 구현할 수 있다면 충분합니다(베塞尔 곡선은 다양한 용도로 사용됩니다. 페이지 전환 효과도 이를 통해 구현할 수 있습니다.). 주로 path.rQuadTo(x1,y1,x2,y2); 종점 (x2를 변경합니다.2), 보조 제어점 (x1를 변경합니다.1의 베塞尔 곡선을 그립니다. 따라서 y 값을 지속적으로 변경하여1의 위치를 통해 물파 효과를 그릴 수 있습니다.
먼저 더블 클릭 이벤트가 아닌지 확인합니다:
더블 클릭을 누를 경우: 변수 d를 설정하고 (d의 값이 num에 의해 변경됨, num은 handler에서 지속적으로 감소됨. num--;), 베塞尔 곡선을 그려서 물파 효과를 구현합니다.
단击을 누를 경우: count 값을 설정하고 (handler에서 변경됨. count--;), count가 나누어 떨어지는지 먼저 판단합니다.2총 구분, 이 두 개의 베塞尔곡선을 교차하여 그려냅니다. (이 두 개의 베塞尔곡선은 정 반대로), 물파가 흔들리는 효과를 만들어냅니다。
(for문을 사용하여 물파의波数를 구현합니다. path.rQuadTo(); 한 쌍은 한 번에 물파를 만들 수 있습니다. 직접 확인해 보세요)
if (!isSingleTap){ float d=(1-(float)num/(100/2))*10; for (int i=0;i<3;i++) { path.rQuadTo(10,-d,20,0); path.rQuadTo(10,d,20,0); } } else { float d=(float)count/50*10; if (count%2==0){ for (int i=0;i<=3;i++) { path.rQuadTo(10,-d,30,0); path.rQuadTo(10,d,30,0); } } else { for (int i=0;i<=3;i++) { path.rQuadTo(10,d,30,0); path.rQuadTo(10,-d,30,0); } } }
마지막으로 메모리를 해제하는 방법입니다. Manifest 파일에 <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/> 권한。
public void killprocess(){ ActivityManager activityManger=(ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> list=activityManger.getRunningAppProcesses(); if(list!=null) for(int i=0;i<list.size();i++) { ActivityManager.RunningAppProcessInfo apinfo=list.get(i); String[] pkgList=apinfo.pkgList; if(apinfo.importance>ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE) { // Process.killProcess(apinfo.pid); for(int j=0;j<pkgList.length;j++) { boolean flag=pkgList[j].contains("com.example.yyh.animation")360");//현재 애플리케이션인지 확인해야 합니다. 그렇지 않으면 현재 애플리케이션도 종료될 수 있습니다. if(!flag){ activityManger.killBackgroundProcesses(pkgList[j]); } } } }
4. FloatMenuView의 구현.
1. float_menuview.xml을 생성합니다;그其中包括 ImageView+TextView+사용자 정의된 MyProgreeView。
끝부분 창이 클릭 가능하게 설정되어야 합니다. android:clickable="true";
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#"33000000" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="#F0"2F3942" android:layout_alignParentBottom="true" android:id="@"+id/ll" android:clickable="true" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:layout_width="50dp" android:layout_height="50dp" android:src="@mipmap/ic_launcher" android:layout_gravity="center_vertical" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:textColor="#c"93944" android:text="360가속구구" android:layout_gravity="center_vertical" /> </LinearLayout> <com.example.yyh.animation360.view.MyProgreeView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10dp" /> </LinearLayout> </RelativeLayout>
2. FloatMenuView를 조건에 따라, (wm.addView(view, params);를 사용하여 view를 창에 추가합니다.
wm.remove(view,params);를 사용하여 view를 창에서 제거함으로써, view의 보이기와 숨기기를 처리하는 메서드를 사용합니다.
TranslateAnimation 클래스는 바닥 창이 진입할 때의 애니메이션 효과를 설정하는 데 사용됩니다. TranslateAnimation(int fromXType,float fromXValue,int toXType,float toXValue,int fromYType,float fromYValue,int toYType,float toYValue)
int fromXType: X축 방향의 시작 참조 값은 다음과 같습니다3옵션。(1.Animation.ABSOLUTE: 구체적인 좌표 값입니다. 절대적인 스크린 픽셀 단위입니다.
2.Animation.RELATIVE_TO_SELF: 상대적 자신 좌표 값입니다.3.Animation.RELATIVE_TO_PARENT: 상대적 부모 컨테이너의 좌표 값입니다。)
float fromXValue: 두 번째 파라미터는 첫 번째 파라미터의 시작 값입니다(예를 들어 첫 번째 파라미터가 Animation.RELATIVE_TO_SELF로 설정되면, 두 번째 파라미터는 0.1f, 이는 자신의 좌표 값을 0으로 곱하는 것을 의미합니다.1);
int toXType: X축 방향의 끝점 참조 값은 다음과 같습니다3처음 파라미터와 동일한 옵션입니다.
float toValue: 네 번째 파라미터는 세 번째 파라미터의 시작 값입니다.
Y축 방향의 파라미터는 동일합니다. 시작점+끝점;(각 파라미터의 다음 파라미터는 이전 파라미터의 시작 값입니다。)
이 뷰에 OnTouchListener를 설정하고, OnTouch 이벤트는 마지막에 반드시 false를 반환해야 합니다. 이렇게 하면 다른 모든 부분을 클릭할 때, 바닥 창이 숨겨지고, 실시간 볼륨이 표시되며, 바닥 창을 클릭하면 변화가 없고, 바닥 창의 볼륨을 클릭하면 클릭과 더블 클릭 이벤트가 발생합니다.
View view =View.inflate(getContext(), R.layout.float_menuview,null); LinearLayout linearLayout= (LinearLayout) view.findViewById(R.id.ll); translateAnimation = new TranslateAnimation(Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,1.0f,Animation.RELATIVE_TO_SELF,0); translateAnimation.setDuration(500); translateAnimation.setFillAfter(true); linearLayout.setAnimation(translateAnimation); view.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { FloatViewManager manager=FloatViewManager.getInstance(getContext()); manager.hideFloatMenuView(); manager.showFloatCircleView(); return false; } }); addView(view);
5.MyFloatService
用它来创建FloatVIewManager单例,管理悬浮小球+创建和删除手机底部窗口。
public class MyFloatService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { //用它来开启FloatViewManager FloatViewManager manager=FloatViewManager.getInstance(this); manager.showFloatCircleView(); super.onCreate(); } }
6.MainActivity 구현
intent을 정의하고, 서비스를 시작합니다(서비스에서 WindowManager 싱글턴 객체를 생성하여 튜브 비디오와 핸드폰 바닥 창의 관리를 합니다. 현재의 activity를 닫습니다。)
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void startService(View view){ Intent intent=new Intent(this, MyFloatService.class); startService(intent); finish(); } }
이것이 본 문서의 모든 내용입니다. 여러분의 학습에 도움이 되길 바랍니다. 또한, 여러분의 지원과 응원을 많이 부탁드립니다.
고지: 본 문서의 내용은 인터넷에서 가져왔으며, 저작권자는 누구인지 알 수 없습니다. 인터넷 사용자가 자발적으로 기여하고 자신의 사이트에 업로드한 내용입니다. 이 웹사이트는 소유권을 가지고 있지 않으며, 인공적인 편집 처리를 하지 않았으며, 관련 법적 책임도 부담하지 않습니다. 저작권 침해가 의심되는 내용을 발견하면, 다음 이메일 주소로 메일을 보내 주시기 바랍니다: notice#oldtoolbag.com(보내실 때는 #을 @으로 변경하시기 바랍니다. 신고를 해 주시고 관련 증거를 제공하시면, 확인되면 해당 웹사이트는 즉시 저작권 침해 내용을 삭제할 것입니다。)